В РАЗРАБОТКЕНЕ ДЛЯ НОВИЧКОВНЕ ОБЯЗАТЕЛЬНО
Разработчику
Архитектору
Инженеру
Справочник по gRPC
1. Общая структура .proto-файла
Прототип .proto-файла в proto3 (текущая рекомендованная версия):
syntax = "proto3";
package example.v1;
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option go_package = "example/v1;examplev1";
option java_multiple_files = true;
option java_package = "com.example.v1";
option java_outer_classname = "ExampleProto";
option csharp_namespace = "Example.V1";
option objc_class_prefix = "EXM";
option optimize_for = SPEED;
// Внешние определения (custom options, расширения — только proto2)
// extend google.protobuf.MessageOptions { ... }
message MyRequest {
string user_id = 1;
int32 page = 2;
}
message MyResponse {
repeated string items = 1;
google.protobuf.Timestamp created_at = 2;
}
service ExampleService {
rpc GetItems(MyRequest) returns (MyResponse) {
option (google.api.http) = {
get: "/v1/items"
};
}
}
2. Директивы верхнего уровня
| Директива | Описание | Поддержка |
|---|
syntax = "proto2" / "proto3" | Указывает версию протобафа. Обязательна. | proto2/proto3 |
package <name> | Логическое пространство имён. Влияет на имена классов в кодогенерации. | Обе |
import "<path>" | Импорт других .proto. Импортируемые файлы должны быть доступны в --proto_path. | Обе |
option <name> = <value>; | Глобальные (файловые) опции. | Обе |
extend ... | Расширения сообщений (только proto2, deprecated в proto3). | Только proto2 |
Основные file-level options
| Опция | Тип | Значение по умолчанию / примечание | Где влияет |
|---|
java_package | string | Не задан → берётся package. Рекомендуется явно указывать. | Java-генерация |
java_outer_classname | string | Базовое имя файла + OuterClass. | Java — имя внешнего класса-контейнера |
java_multiple_files | bool | false | true → каждый message/service как отдельный класс |
java_string_check_utf8 | bool | false | true → проверка корректности UTF-8 при парсинге строк |
optimize_for | enum (SPEED, CODE_SIZE, LITE_RUNTIME) | SPEED | Влияет на объём и скорость сгенерированного кода (proto2/proto3) |
go_package | string | Не задан → package. Формат: "path;alias" | Go — путь импорта и псевдоним пакета |
csharp_namespace | string | Не задан → package. | C# — namespace |
objc_class_prefix | string | Обязателен в proto3 для Objective-C. Должен быть 3+ буквы, уникальный. | Swift/ObjC |
php_namespace | string | Не задан → package. | PHP |
php_metadata_namespace | string | — | PHP: где хранить метаданные |
php_class_prefix | string | — | PHP: префикс имён классов |
ruby_package | string | — | Ruby |
swift_prefix | string | — | Swift |
deprecated | bool | false | Помечает файл как устаревший (влияет на генерируемый код, если поддерживается языком) |
⚠️ Некоторые опции (например, cc_api_version, cc_generic_services) устарели или не поддерживаются в proto3 (в частности, generic services отключены по умолчанию и deprecated).
3. Сообщения (message)
Базовая структура
message User {
option (my_file_options).id = 123;
option deprecated = true;
string email = 1 [(validate.rules).string.email = true];
int32 age = 2 [jstype = JS_STRING];
oneof contact_info {
string phone = 3;
string telegram = 4;
}
repeated string tags = 5 [packed = true];
map<string, string> metadata = 6;
}
3.1. Поля сообщения
Формат поля:
[field_behavior] [field_type] field_name = field_number [field_options];
| Компонент | Обязательный? | Описание |
|---|
field_behavior | Нет (только proto3) | optional (proto3), required (proto2 only), repeated |
field_type | Да | scalar, message, enum, map<...>, oneof-члены |
field_name | Да | snake_case по convention (но не enforced) |
field_number | Да | int ≥ 1. 1–15 — 1 байт в wire format, 16–2047 — 2 байта. Зарезервированы 19000–19999 (wire type tags). |
field_options | Нет | [key = value, ...] |
3.1.1. field_behavior
| Значение | proto2 | proto3 | Примечание |
|---|
required | ✅ | ❌ | Deprecated даже в proto2 (начиная с Protobuf 3.3.0; генерирует warnings). Избегать. |
optional | ✅ (по умолчанию) | ✅ (явно с proto 3.12+, включено по умолчанию с 3.15) | В proto3: включает presence tracking (has_field). |
repeated | ✅ | ✅ | Для скаляров в proto3: packed=true по умолчанию. |
🔹 В proto3 до 3.12: optional нельзя было писать (но optional-поля существовали в runtime через HasField). С 3.15 — optional включён по умолчанию (т.е. optional int32 x = 1; допустим).
🔹 required считается антипаттерном: нарушает forward/backward совместимость (удаление → поломка парсинга).
3.1.2. Scalar types
| .proto type | C++ | Java | Python | Go | C# | Dart | wire type | JSON encode |
|---|
double | double | double | float | float64 | double | double | 1 | number |
float | float | float | float | float32 | float | double | 5 | number |
int32 | int32 | int | int | int32 | int | int | 0 | number |
int64 | int64 | long | int | int64 | long | int | 0 | string¹ |
uint32 | uint32 | int | int | uint32 | uint | int | 0 | number |
uint64 | uint64 | long | int | uint64 | ulong | int | 0 | string¹ |
sint32 | int32 | int | int | int32 | int | int | 0 | number |
sint64 | int64 | long | int | int64 | long | int | 0 | string¹ |
fixed32 | uint32 | int | int | uint32 | uint | int | 5 | number |
fixed64 | uint64 | long | int | uint64 | ulong | int | 1 | string¹ |
sfixed32 | int32 | int | int | int32 | int | int | 5 | number |
sfixed64 | int64 | long | int | int64 | long | int | 1 | string¹ |
bool | bool | boolean | bool | bool | bool | bool | 0 | true/false |
string | string | String | str/unicode | string | string | String | 2 | string |
bytes | string | ByteString | bytes | []byte | ByteString | Uint8List | 2 | base64 string |
¹ — Для int64, uint64, fixed64, sfixed64 в JSON: строка, чтобы избежать потери точности в JS (Number.MAX_SAFE_INTEGER = 2⁵³−1). Это регулируется опцией jstype (см. ниже).
3.1.3. jstype (field option, proto3 only)
Управляет JSON-представлением 64-битных типов:
int64 id = 1 [jstype = JS_STRING]; // → "1234567890123456789"
int64 id = 1 [jstype = JS_NUMBER]; // → 1234567890123456789 (потенциально unsafe!)
int64 id = 1 [jstype = JS_NORMAL]; // → как по умолчанию (строка)
| Значение | JSON | Риски |
|---|
JS_NORMAL (default) | "123" | Безопасно |
JS_STRING | "123" | Безопасно, явно |
JS_NUMBER | 123 | Потеря точности в JS при значении > 2⁵³−1 |
4. Составные типы полей
4.1. oneof
Группа полей, из которых только одно может быть установлено в один момент времени. Используется для union-подобной семантики.
Синтаксис
message LoginRequest {
oneof method {
string username = 1;
string email = 2;
int64 phone = 3;
}
string password = 4;
}
Особенности
| Свойство | Описание |
|---|
Имя oneof | Опционально. Без имени — анонимный oneof (не рекомендуется). Имя используется в сгенерированном коде (WhichOneof("method")). |
| Поля внутри | Любые типы, включая message, enum, scalar. Но не repeated, map, group (deprecated). |
| Field numbers | Общее пространство с другими полями сообщения — не должны пересекаться. |
| Наследование | oneof не может быть унаследован (в protobuf нет наследования сообщений). |
| Кодогенерация | В большинстве языков создаётся enum-case (например, MethodOneofCase в C#/Java) и setter/getter методы (setUsername(), getEmail() и т.п.). |
| Очистка | Установка нового поля внутри oneof автоматически сбрасывает предыдущее. |
| JSON | Сериализуется как обычные поля. При десериализации: последнее установленное значение побеждает (если несколько — поведение неопределено). |
| proto2/proto3 | Поддерживается в обеих версиях. |
Опции oneof (редко используются, но доступны)
- Нет официальных field/message options для
oneof напрямую.
- Можно применить custom options к отдельным полям внутри
oneof.
Пример использования в коде (C# с Google.Protobuf)
var req = new LoginRequest { Email = "user@example.com" };
Console.WriteLine(req.MethodCase);
req.Username = "admin";
Console.WriteLine(req.MethodCase);
4.2. map<K, V>
Ассоциативный массив. Компилируется в хеш-таблицы в целевых языках.
Синтаксис
map<string, string> metadata = 1;
map<int32, User> user_cache = 2;
map<string, google.protobuf.Value> dynamic_data = 3;
Ограничения
| Параметр | Требования |
|---|
K (ключ) | Только string, int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool. Не float, double, bytes, message, enum. |
V (значение) | Любые типы, включая message, enum, oneof-содержащие сообщения. Но не map, repeated, group. |
| Field number | Обычное поле. Занимает один номер. Внутренне реализован как repeated message с полями key, value. |
Внутренне эквивалент:
message MapFieldEntry {
option map_entry = true;
optional K key = 1;
optional V value = 2;
}
repeated MapFieldEntry map_field = N;
🔹 map_entry = true — служебная опция, задаётся автоматически компилятором.
Поведение
| Аспект | Комментарий |
|---|
| Порядок | Не гарантируется (как и в хеш-таблицах). |
| Повторяющиеся ключи | При парсинге: последнее значение побеждает. |
| Производительность | map эффективнее repeated KeyValue, но менее гибок (нельзя добавить third-party metadata к записи). |
| Совместимость | Можно безопасно изменить тип поля с repeated KeyValue → map, если структура KeyValue соответствует key/value. Обратное — нет. |
| proto2/proto3 | Доступен только в proto3+. В proto2 — только через repeated message с map_entry=true. |
4.3. repeated
Список/массив значений.
Синтаксис
repeated string tags = 1;
repeated User friends = 2;
Опции
| Опция | Тип | Значение по умолчанию (proto3) | Примечание |
|---|
packed | bool | true для скалярных типов | Влияет на wire format: упаковка в один length-delimited chunk (экономия места). Для message/enum — всегда false (нельзя). Устаревшая, но допустимая конструкция: [packed = false]. |
Особенности
| Аспект | Подробности |
|---|
| Пустой список | Не передаётся по сети (экономия bandwidth). При десериализации — пустая коллекция (не null). |
| Null-элементы | Недопустимы. Все элементы не-nullable. |
| Производительность | repeated message — эффективнее, чем map при известном ключе (если ключ — индекс или enum). |
| Обратная совместимость | Безопасно: добавление repeated поля не ломает старые клиенты (игнорируют). Удаление — тоже безопасно, если поле не критично. |
5. enum
Перечисления.
Базовый синтаксис
enum StatusCode {
STATUS_UNKNOWN = 0;
STATUS_OK = 200;
STATUS_NOT_FOUND = 404;
}
message Response {
StatusCode code = 1;
}
Правила и ограничения
| Правило | Значение |
|---|
| Первое значение | Должно быть = 0. В proto3 — это значение по умолчанию для неинициализированных полей. В proto2 — допустимо указать option allow_alias = true, чтобы разрешить дубли значений (но не имён). |
| Значения | 32-битные signed int. Диапазон: −2³¹ … 2³¹−1. |
| Имена | UPPER_SNAKE_CASE по convention. |
| Расширения | В proto2: extend StatusCode { ... } (deprecated). В proto3 — нельзя. |
| Reserved | Можно резервировать значения/имена: reserved 10, 11 to 15; reserved "OLD_CODE"; |
Опции enum и enum-value
| Уровень | Опция | Тип |
|---|
| enum-level | allow_alias | bool (только proto2) |
| enum-level | deprecated | bool |
| value-level | deprecated | bool |
| value-level | option (my_options).weight = 5; | custom option |
Совместимость
- Добавление нового значения enum — безопасно (старые клиенты получат неизвестное значение → в proto3:
UNRECOGNIZED, в proto2: UNKNOWN_ENUM_VALUE_StatusCode_999).
- Удаление значения — небезопасно, если оно использовалось в сериализованных данных.
- Изменение числового значения — категорически запрещено.
JSON mapping
| Поле | Значение | JSON |
|---|
code = STATUS_OK | 200 | "STATUS_OK" (по имени) или 200 (по числу), в зависимости от настроек сериализатора. |
| Неизвестное значение (999) | — | "UNKNOWN_STATUS_CODE_999" (или число, если enum_as_int). |
🔹 В gRPC-JSON трансляторах (например, grpc-gateway) по умолчанию используется имя, а не число.
6. reserved
Запрещает использование определённых имён или номеров полей/значений enum — для предотвращения несовместимых изменений.
Применение
message User {
reserved 2, 15, 9 to 11;
reserved "login", "password_hash";
string email = 1;
// int32 age = 2; ← ошибка компиляции: номер 2 зарезервирован
// string login = 3; ← ошибка: имя "login" зарезервировано
}
Где можно использовать
| Контекст | Синтаксис | Пример |
|---|
message | reserved <field_number>, <range>, "<name>"; | reserved 4, 5 to 7, "old_field"; |
enum | reserved <value>, <range>, "<name>"; | reserved 500, 501 to 599, "DEPRECATED_CODE"; |
Важно
- Нельзя резервировать пересекающиеся диапазоны.
- Резервирование не влияет на wire format — только на этапе компиляции
.proto.
- Необратимо: однажды зарезервированное нельзя "разрезервировать" без поломки совместимости.
7. Extensions (только proto2, deprecated)
Механизм для расширения сообщений без изменения их исходного определения.
Синтаксис (proto2)
// base.proto
message BaseMessage {
extensions 100 to 199;
}
// ext.proto
extend BaseMessage {
optional string extended_field = 101;
}
Использование в коде (Java, proto2)
BaseMessage msg = BaseMessage.newBuilder()
.setExtension(Ext.extendedField, "value")
.build();
String val = msg.getExtension(Ext.extendedField);
Почему deprecated
- Нарушает читаемость: структура сообщения не видна из
.proto.
- Плохо масштабируется.
- Не поддерживается в proto3 (кроме
Any, см. ниже).
- Не поддерживается большинством современных фреймворков (например, gRPC Gateway, Envoy, Linkerd).
✅ Альтернатива: google.protobuf.Any (см. раздел Well-Known Types).
8. Well-Known Types (google.protobuf.*)
Стандартные типы, поставляемые с protobuf. Импортируются как:
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
// ...
| Тип | Описание | Пример использования |
|---|
Any | Упаковка любого сообщения по type_url (например, type.googleapis.com/example.v1.User). | Полиморфные API, события, generic payload. |
Timestamp | Точное время с точностью до наносекунд (RFC 3339). | created_at, last_modified. |
Duration | Продолжительность (например, 3.5s, 2d). | Таймауты, ttl, интервалы. |
Struct, Value, ListValue, NullValue | Динамические JSON-подобные структуры. | Конфиги, метаданные, API с динамической схемой. |
FieldMask | Частичное обновление (update_mask в UpdateRequest). | PATCH-операции, selective fetch. |
Empty | Пустое сообщение (аналог void). | Ping(), Delete() без входных данных. |
DoubleValue, FloatValue, Int64Value, UInt32Value, ... (wrapper types) | Обёртки для nullable скаляров (optional до proto 3.12). | repeated google.protobuf.Int32Value items; где null допустим. |
Api, Method, Syntax, SourceContext | Описание API (для инструментов). | Интроспекция, генерация документации. |
Type, Enum, EnumValue, Option | Метаописание protobuf-схемы. | Рефлексия, динамический парсинг. |
Пример: Any
import "google/protobuf/any.proto";
message Event {
string event_id = 1;
google.protobuf.Any payload = 2;
}
В коде (C#):
var user = new User { Id = "u1" };
var any = Any.Pack(user);
var evt = new Event { Payload = any };
if (evt.Payload.Is(User.Descriptor)) {
var unpacked = evt.Payload.Unpack<User>();
}
⚠️ Any требует, чтобы получатель знал схему упакованного типа (или имел DescriptorPool). Не подходит для полностью динамических систем без реестра типов.
9. Сервисы и методы
9.1. Объявление сервиса
service UserService {
option (google.api.default_host) = "api.example.com";
option (google.api.oauth_scopes) = "https://www.googleapis.com/auth/userinfo.email";
rpc GetUser(GetUserRequest) returns (User) {
option (google.api.http) = {
get: "/v1/users/{user_id}"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Получить пользователя по ID";
description: "Возвращает полную информацию о пользователе.";
tags: "Users";
};
}
rpc StreamEvents(StreamEventsRequest) returns (stream Event) {}
rpc UploadAvatar(stream AvatarChunk) returns (AvatarMetadata) {}
rpc Chat(stream Message) returns (stream Message) {}
}
9.2. Типы методов
| Тип | Сигнатура | Назначение | Примеры |
|---|
| Unary | rpc Method(Request) returns (Response) | Один запрос → один ответ. | GetUser, CreateOrder, Ping |
| Server-Streaming | rpc Method(Request) returns (stream Response) | Один запрос → поток ответов. | WatchChanges, SubscribeToNotifications, ListLogEntries |
| Client-Streaming | rpc Method(stream Request) returns (Response) | Поток запросов → один ответ. | UploadFile, BatchInsert, CollectMetrics |
| Bidirectional Streaming | rpc Method(stream Request) returns (stream Response) | Полный duplex. | Chat, CollaborativeEditing, RealtimeAnalytics |
Особенности работы со streaming:
- Все streaming-методы требуют явного управления потоком (backpressure, cancellation).
- Протокол: HTTP/2, frame-based (
DATA, HEADERS, RST_STREAM).
- Сервер обязан завершать поток вызовом
SendAndClose (unary) или Send/CloseSend + Recv в цикле (streaming).
- Клиент может отменить вызов через
CancellationToken (C#), Context (Go), AbortController (JS/TS).
10. Опции методов и сервисов
10.1. Стандартные и широко используемые опции
| Опция | Уровень | Контекст | Описание |
|---|
(google.api.http) | method | annotations.proto | Сопоставление gRPC ↔ HTTP/JSON (REST-шлюз). Поддерживает get, post, put, patch, delete, body, additional_bindings. |
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) | method | protoc-gen-openapiv2 | Переопределения для OpenAPI/Swagger (summary, description, tags, parameters, responses). |
(google.api.method_signature) | method | annotations.proto | Набор полей, формирующих «сигнатуру» (для генерации методов в SDK, напр., get_user(user_id) вместо get_user(request)). |
(google.api.resource_definition) / (google.api.resource) | file/message | resource.proto | Описание иерархии ресурсов (например, projects/{project}/locations/{location}/clusters/{cluster}). |
(google.api.field_behavior) | field | field_behavior.proto | Семантика поля: REQUIRED, OUTPUT_ONLY, INPUT_ONLY, IMMUTABLE, OPTIONAL. Используется валидаторами и генераторами документации. |
(grpc.method_idempotency_level) | method | gRPC core | IDEMPOTENT / NO_SIDE_EFFECTS / UNKNOWN. Влияет на retry-поведение (если IDEMPOTENT, клиент может повторить при UNAVAILABLE). |
(validate.rules) | field/message | protoc-gen-validate | Правила валидации: string.min_len, int.gt, email, ipv4, uuid, contains, in, pattern и др. |
Пример: HTTP mapping + валидация + OpenAPI
import "google/api/annotations.proto";
import "protoc-gen-validate/validate.proto";
import "grpc/gateway/protoc_gen_openapiv2/options/annotations.proto";
message CreateUserRequest {
string email = 1 [
(validate.rules).string = {
email: true,
min_len: 5,
max_len: 254
},
(google.api.field_behavior) = REQUIRED
];
string name = 2 [(validate.rules).string.min_len = 1];
}
rpc CreateUser(CreateUserRequest) returns (User) {
option (google.api.http) = {
post: "/v1/users"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Создать пользователя";
description: "Регистрирует нового пользователя. Email должен быть уникальным.";
tags: "Users";
responses: {
key: "409"
value: {
description: "Пользователь с таким email уже существует.";
schema: {
json_schema: { ref: "#/definitions/google.rpc.Status" }
}
}
};
};
}
11. Соглашения по именованию и структуре (AIP, Google API Style Guide)
⚠️ Не являются частью протокола, но критически важны для совместимости, документации и инструментария.
11.1. Имена пакетов и файлов
| Элемент | Рекомендация | Пример |
|---|
package | lowercase, . как разделитель, версия в конце (v1, v1alpha1, v1beta1) | example.user.v1 |
| Файл | lower_snake_case, .proto, группа по домену/версии | user_service.proto, user_resource.proto |
| Импорт | Организован по слоям: стандартные → well-known → внешние → локальные | google/... → third_party/... → example/... |
11.2. Имена сообщений и полей
| Элемент | Рекомендация | Примечание |
|---|
| Сообщение | UpperCamelCase, существительное или прилагательное + Request/Response/Metadata | CreateUserRequest, ListUsersResponse, User |
| Поле | snake_case, кратко и однозначно | user_id, created_at, display_name |
oneof | snake_case, отражает семантику | contact_info, auth_method |
| Enum | UPPER_SNAKE_CASE, префикс с именем enum (избегать конфликтов) | STATUS_OK, CODE_INVALID_ARGUMENT |
11.3. Стандартные сообщения (AIP-131/132/133/134/135)
| Операция | Рекомендуемое имя метода | Request | Response |
|---|
| Get | Get{Resource} | {Resource}Name → string name | {Resource} |
| List | List{Resources} | string parent, int32 page_size, string page_token | repeated {Resource} {resources}, string next_page_token |
| Create | Create{Resource} | string parent, {Resource} {resource}, string {resource}_id | {Resource} |
| Update | Update{Resource} | {Resource} {resource}, FieldMask update_mask | {Resource} |
| Delete | Delete{Resource} | string name | google.protobuf.Empty |
| Повторная отправка (replay-safe) | Добавить string request_id в Request | — | — |
🔹 name — каноническое имя ресурса: projects/123/users/456.
12. Кастомные опции (custom options)
Позволяют расширять .proto схемы аннотациями для генераторов, линтеров, runtime.
12.1. Объявление кастомной опции
// my_options.proto
syntax = "proto3";
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
string my_field_tag = 50001;
}
extend google.protobuf.MethodOptions {
bool my_method_audit = 50002;
}
12.2. Использование
import "my_options.proto";
message User {
string email = 1 [(my_field_tag) = "PII"];
}
service UserService {
rpc GetUser(GetUserRequest) returns (User) {
option (my_method_audit) = true;
}
}
12.3. Практическое применение
| Цель | Инструмент | Пример опций |
|---|
| Валидация | protoc-gen-validate | (validate.rules).string.email = true |
| OpenAPI | protoc-gen-openapiv2 | (grpc.gateway...) |
| Авторизация | Custom middleware | (my.auth).roles = ["admin", "support"] |
| Логгирование | Custom interceptor | (my.log).level = "INFO" |
| Идемпотентность | Client generator | (my.idempotency).key_field = "request_id" |
| Метрики | Observability SDK | (my.metrics).name = "user_get_latency" |
⚠️ Номера расширений (50001+) должны быть уникальны в рамках проекта. Рекомендуется регистрировать в центральном реестре (например, internal/api-options.proto).
13. Эволюция схемы: правила обратной и прямой совместимости
13.1. Безопасные изменения (forward & backward compatible)
| Изменение | proto2 | proto3 | Примечание |
|---|
Добавить optional/repeated поле | ✅ | ✅ | Старые клиенты игнорируют. Новые — получают default/empty. |
Добавить oneof-поле | ✅ | ✅ | Старые клиенты видят 0/"". |
Добавить map | ✅ | ✅ | Внутренне — repeated message. |
| Добавить enum-значение | ✅* | ✅* | * — клиенты должны обрабатывать UNRECOGNIZED. |
Изменить optional → repeated | ❌ | ⚠️ | Только если поле не использовалось ранее. |
Добавить reserved | ✅ | ✅ | Только для будущего использования. |
13.2. Небезопасные изменения (ломают совместимость)
| Изменение | Последствие |
|---|
| Изменить номер поля | Неправильная десериализация (поле A читается как B). |
Изменить тип поля (например, string → int32) | InvalidProtocolBufferException, UnmarshallingError. |
Удалить required поле (proto2) | Крах парсинга у клиентов, отправляющих старый формат. |
| Переименовать enum-значение без резервирования старого | Старые клиенты отправляют старое имя → UNKNOWN. |
13.3. Стратегии миграции
- Двойная запись / двойное чтение (dual writing / dual reading).
- Версионирование API:
v1, v1beta1, v2.
FieldMask: клиенты явно указывают, какие поля хотят читать/писать.
oneof для альтернативных форматов:
message Request {
oneof version {
LegacyData v1 = 1;
NewData v2 = 2;
}
}
14. Обработка ошибок
14.1. Стандартный статус gRPC (grpc.Status)
| Код (int) | Имя | HTTP-маппинг | Когда использовать |
|---|
0 | OK | 200 | Успех. |
1 | CANCELLED | 499 | Клиент отменил. |
2 | UNKNOWN | 500 | Неизвестная ошибка (логгировать!). |
3 | INVALID_ARGUMENT | 400 | Некорректный запрос (валидация). |
4 | DEADLINE_EXCEEDED | 504 | Таймаут. |
5 | NOT_FOUND | 404 | Ресурс не найден. |
6 | ALREADY_EXISTS | 409 | Конфликт уникальности. |
7 | PERMISSION_DENIED | 403 | Нет прав (не аутентификация!). |
8 | RESOURCE_EXHAUSTED | 429 | Лимиты (rate, quota, memory). |
9 | FAILED_PRECONDITION | 400 | Нарушено предусловие (например, update_mask без полей). |
10 | ABORTED | 409 | Конфликт транзакции (CAS failed). |
11 | OUT_OF_RANGE | 400 | Выход за границы (например, page_size > 1000). |
12 | UNIMPLEMENTED | 501 | Метод не реализован. |
13 | INTERNAL | 500 | Ошибка сервера (баг). |
14 | UNAVAILABLE | 503 | Сервис недоступен (retryable). |
15 | DATA_LOSS | 500 | Потеря данных (редко). |
16 | UNAUTHENTICATED | 401 | Не аутентифицирован. |
14.2. Детализация ошибки: google.rpc.Status
message Status {
int32 code = 1;
string message = 2;
repeated google.protobuf.Any details = 3;
}
Пример details:
google.rpc.BadRequest — указание на конкретное поле.
google.rpc.QuotaFailure — какая квота исчерпана.
google.rpc.PreconditionFailure — какие условия нарушены.
- Кастомное сообщение — для внутренних нужд.
14.3. Передача ошибок в клиентах
- C#:
RpcException.StatusCode, RpcException.Trailers, RpcException.Status.Details.
- Go:
status.FromError(err), st.Details().
- Java:
StatusRuntimeException.getStatus().
- Python:
grpc.RpcError.code(), details.
15. Кодогенерация: инструменты, плагины и стратегии
15.1. Базовый инструментарий
| Инструмент | Назначение | Версия | Особенности |
|---|
protoc | Официальный компилятор Protocol Buffers | ≥ v3.0 (proto3), ≥ v21.0 (proto3+/edition2023) | Требует явной установки protoc-gen-* плагинов. Не имеет встроенного управления зависимостями. |
buf | Универсальный инструмент для работы с protobuf (lint, format, generate, breaking, mod) | ≥ v1.0 | Включает встроенный protoc, управление модулями, CI-интеграция, строгая проверка совместимости. |
protoc-gen-* | Плагины для генерации кода | Язык-зависимые | Официальные: grpc_***_plugin (C++, Java, Python, Go, Ruby, C#, PHP, Dart, Node.js). |
Минимальная команда protoc:
protoc \
--proto_path=src/proto \
--csharp_out=gen/csharp \
--csharp_opt=base_namespace=Example.V1 \
--grpc_out=gen/csharp \
--plugin=protoc-gen-grpc=/usr/local/bin/grpc_csharp_plugin \
src/proto/user_service.proto
Эквивалент в buf.gen.yaml:
version: v1
plugins:
- name: csharp
out: gen/csharp
opt:
- base_namespace=Example.V1
- name: grpc_csharp
out: gen/csharp
path: grpc_csharp_plugin
→ buf generate
15.2. Популярные генераторы (альтернативы официальным)
| Генератор | Язык | Плюсы | Минусы | Совместимость |
|---|
gRPC-Web (protoc-gen-grpc-web) | TS/JS | Работает в браузере через HTTP/1.1 (proxy required) | Требует envoy/gateway, нет streaming в браузере (только unary) | Совместим с gRPC-серверами |
connect-go / connect-web (Buf) | Go/TS | Совместимость с gRPC и Connect (Protocol + JSON), поддержка streaming в браузере (via WebSockets или HTTP/2), меньше boilerplate | Менее «каноничный» для чистого gRPC | Частично обратно совместим |
betterproto (Python) | Python | Typed dataclasses, async-first, меньше boilerplate | Не 100% совместим с grpcio API | Совместим на wire level |
ts-proto | TypeScript | Поддержка NestJS, TypeORM, strict types, enums as union types | Требует отдельной настройки для сервера | Совместим на wire level |
protoc-gen-validate | Мультиязыковой | Runtime validation (Go, Java, Python, JS, C++) | Добавляет зависимость, накладные расходы на валидацию | Является extension, не заменяет генератор |
🔹 Рекомендация: для production-систем — официальные генераторы (google.golang.org/protobuf, Grpc.Tools в .NET, grpc-java). Альтернативы — при особых требованиях (браузер, строгая типизация).
15.3. Контроль версий схем: buf.lock и deps
buf поддерживает модули с фиксированными хешами:
// buf.yaml
version: v1
name: buf.build/example/user
deps:
- buf.build/googleapis/googleapis
- buf.build/grpc-ecosystem/grpc-gateway
breaking:
use:
- FILE
lint:
use:
- DEFAULT
→ buf mod update фиксирует buf.lock с SHA256-хешами зависимостей.
Это гарантирует повторяемость генерации: все разработчики и CI используют одинаковые версии googleapis, protoc, и т.д.
16.1. Форматы сериализации
| Формат | MIME-Type | Поддержка | Примечания |
|---|
| Binary (proto) | — | Все реализации | Компактный, быстрый, binary-safe. Content-Type: application/grpc (подразумевает binary protobuf). |
| JSON | application/json | grpc-gateway, connect, envoy | Требует google/api/http.proto, FieldMask → updateMask (snake_case). 64-bit → строки по умолчанию. |
| TextFormat | — | Инструменты (protoc --encode), отладка | Человекочитаемый, не для продакшена. Пример: user_id: "u1" created_at: { seconds: 1732000000 }. |
16.2. Сжатие
| Алгоритм | HTTP-заголовок | Поддержка | Накладные расходы |
|---|
gzip | grpc-encoding: gzip, grpc-accept-encoding: gzip | Почти все (C++, Go, Java, .NET) | ЦП: ~10–20% CPU, выигрыш 60–80% размера. |
deflate | grpc-encoding: deflate | Частичная | Менее эффективен, чем gzip. |
snappy | grpc-encoding: snappy | Go, C++, Java (часто custom) | Быстрый, но меньшая степень сжатия (~20–30%). |
| Отсутствие | — | По умолчанию | Для маленьких сообщений (<1 KB) — предпочтительно. |
⚠️ Сжатие не применяется к streaming-фреймам по отдельности — оно действует на весь поток. Сервер и клиент должны согласовать алгоритм через grpc-accept-encoding.
16.3. HTTP/2 и multiplexing
- gRPC обязательно требует HTTP/2.
- Один TCP-соединение → множество параллельных вызовов (streams).
- Управление потоком:
SETTINGS_INITIAL_WINDOW_SIZE, WINDOW_UPDATE.
- Keepalive: настраивается через
grpc.keepalive_time_ms, grpc.http2.max_pings_without_data.
Keepalive параметры (клиент и сервер):
| Параметр | Значение по умолчанию | Рекомендация prod |
|---|
GRPC_ARG_KEEPALIVE_TIME_MS | INT_MAX (отключено) | 300_000 (5 мин) |
GRPC_ARG_KEEPALIVE_TIMEOUT_MS | 20_000 | 20_000 |
GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS | 0 | 1 (если нужен health-check без вызовов) |
GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA | 2 | 0 (ping только при активности) |
🔹 Без keepalive: промежуточные прокси (nginx, ALB) могут разрывать «тихое» соединение через 60–300 сек.
17. Безопасность
17.1. Транспортный уровень
| Механизм | Описание | Поддержка |
|---|
| TLS | Шифрование канала | Все реализации. Сертификат сервера обязателен. |
| mTLS | Двусторонняя аутентификация (клиент + сервер) | Требует ssl_client_cert, ssl_client_key у клиента. |
| ALTS | Application Layer Transport Security (Google Cloud only) | grpc++_alts (C++), io.grpc.alts (Java). Не для публичных API. |
Пример инициализации сервера (C#):
var cert = X509Certificate2.CreateFromPemFile("cert.pem", "key.pem");
var keyCertPair = new KeyCertificatePair(cert.Export(X509ContentType.Pem), cert.Export(X509ContentType.Pem));
var server = new Server
{
Services = { UserService.BindService(new UserServiceImpl()) },
Ports = { new ServerPort("0.0.0.0", 50051, new SslServerCredentials(new[] { keyCertPair })) }
};
17.2. Аутентификация и авторизация
| Уровень | Метод | Реализация |
|---|
| Транспорт | mTLS, TLS client cert | SslClientCredentials + custom interceptor для проверки CommonName/Subject. |
| Метаданные | Authorization: Bearer <token> | Перехват Context.RequestHeaders в interceptor. |
| JWT | Authorization: Bearer <JWT> | Проверка подписи, issuer, exp, scope/roles в claims. |
| API Key | x-api-key: <key> | Проверка в interceptor или gateway. |
Interceptor (middleware) — пример (Go):
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
tokens := md["authorization"]
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
return handler(ctx, req)
}
17.3. Шифрование данных (application-level)
| Требование | Решение |
|---|
| PII в логах | Не логировать metadata, request, response напрямую. Использовать маскировку (email → u***@e***.com). |
| Данные в базе | Шифрование на уровне приложения (AES-GCM) до сериализации в protobuf. |
bytes-поля | Могут содержать зашифрованные payload (например, encrypted_payload: bytes). |
⚠️ Protobuf не предоставляет встроенной защиты данных. Шифрование — ответственность приложения.
18. Тестирование и отладка
18.1. Инструменты командной строки
| Инструмент | Назначение | Пример |
|---|
grpcurl | curl для gRPC | grpcurl -plaintext localhost:50051 list grpcurl -d '{"user_id":"u1"}' localhost:50051 UserService.GetUser |
ghz | Нагрузочное тестирование | ghz --proto user_service.proto --call UserService.GetUser --insecure localhost:50051 |
bloomrpc | GUI-клиент (устаревает) | — |
grpcui | Веб-интерфейс (на основе protoc reflection) | grpcui -plaintext localhost:50051 |
18.2. Reflection и introspection
Сервер может поддерживать сервис grpc.reflection.v1alpha.ServerReflection:
service ServerReflection {
rpc ServerReflectionInfo(stream ServerReflectionRequest) returns (stream ServerReflectionResponse);
}
→ Позволяет клиентам динамически получать:
- Список сервисов;
- Описание методов;
.proto-файлы (если загружены в FileDescriptorSet).
Включение в Go:
import "google.golang.org/grpc/reflection"
reflection.Register(s)
В .NET:
services.AddGrpcReflection();
app.MapGrpcReflectionService();
18.3. Unit/integration тестирование
| Язык | Подход |
|---|
| C# | Grpc.AspNetCore.Server.ClientFactory, Grpc.Net.Client, TestServer + GrpcChannel.ForAddress(server.BaseAddress) |
| Go | bufconn, grpc.NewServer(), grpc.DialContext(ctx, "bufnet", ...)) |
| Java | InProcessServerBuilder, InProcessChannelBuilder |
| Python | grpc.insecure_channel("localhost:0"), server.add_insecure_port("[::]:0") |
Пример (C#):
[Fact]
public async Task GetUser_ReturnsUser()
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<UserServiceImpl>();
using var server = app.Services.GetRequiredService<IServer>();
var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
{
HttpClient = new HttpClient(new TestHttpMessageHandler(app))
});
var client = new UserService.UserServiceClient(channel);
var resp = await client.GetUserAsync(new GetUserRequest { UserId = "u1" });
Assert.Equal("u1", resp.User.Id);
}
19. Производительность и тюнинг
19.1. Настройки вызова (CallOptions / CallCredentials)
| Параметр | Уровень | Примечание |
|---|
| Deadline / Timeout | Клиент | withDeadlineAfter(5, SECONDS) (Java), deadline: DateTime.UtcNow.AddSeconds(5) (C#). Сервер получает Context.Deadline. Превышение → DEADLINE_EXCEEDED. |
| Cancellation | Клиент | CancellationToken (C#), Context.WithCancel() (Go). Сервер получает Context.IsCancellationRequested. |
| Compression | Клиент/Сервер | CallOptions.WithCompression(Compression.Gzip). Сервер должен поддерживать. |
| WaitForReady | Клиент | CallOptions.WithWaitForReady(true). Не завершать вызов при UNAVAILABLE (например, при старте сервера). Использовать осторожно — может привести к зависанию. |
| MaxInboundMessageSize | Сервер/Клиент | MaxReceiveMessageSize (C#), MaxInboundMessageSize (Go). Защита от OOM. По умолчанию: 4 MB. |
| MaxOutboundMessageSize | Клиент/Сервер | Аналогично. |
19.2. Streaming: управление потоком и batching
| Проблема | Решение |
|---|
| Backpressure | Использовать Send/Recv с await (async) или SendAsync (C# streaming). Не отправлять быстрее, чем клиент читает. |
| Chunking | Для больших данных (файлы): фиксированный размер чанка (например, 64 KB). Избегать repeated bytes → использовать stream Chunk. |
| Batching ответов | На сервере: буферизовать N элементов или T мс, затем Send(). Снижает overhead фреймов. |
| Flow control (HTTP/2) | Настройка initial_window_size, max_concurrent_streams на стороне сервера (nginx: http2_max_concurrent_streams, http2_chunk_size). |
19.3. Пулы и ресурсы
| Компонент | Рекомендации |
|---|
| gRPC-канал (Channel) | Создавать один канал на endpoint и переиспользовать. Дорогая инициализация (TLS handshake, HTTP/2 setup). В .NET: GrpcChannel.ForAddress(...) — singleton. |
| Серверные потоки | Настройка SynchronizationContext (C#), runtime.GOMAXPROCS (Go), grpc.nettySharedGroups (Java). |
| Сериализация | Кэшировать Parser<T> (Java), MessageParser<T> (C#). Избегать повторного парсинга Any. |
| Reflection | Отключать в продакшене (reflection.Register только в dev/staging). |
🔹 Пример пула каналов (C#):
services.AddSingleton<GrpcChannel>(sp =>
GrpcChannel.ForAddress("https://api.example.com", new GrpcChannelOptions
{
Credentials = SslCredentials.Default,
MaxReceiveMessageSize = 100 * 1024 * 1024,
MaxSendMessageSize = 100 * 1024 * 1024
}));
20. Наблюдаемость (Observability)
20.1. Tracing (OpenTelemetry)
gRPC интегрируется с OpenTelemetry через interceptor.
| Язык | Пакет | Примечание |
|---|
| C# | OpenTelemetry.Instrumentation.GrpcNetClient, Grpc.AspNetCore.Server | Автоматически трассирует unary-вызовы. Streaming — частично. |
| Go | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc | Unary и streaming. |
| Java | io.opentelemetry.instrumentation:opentelemetry-grpc-1.6 | Поддержка gRPC ≥1.6. |
Данные трейса:
- Span name:
grpc.client.request / grpc.server.request
- Атрибуты:
rpc.system=grpc, rpc.service=UserService, rpc.method=GetUser, net.peer.ip
- События:
message sent, message received
20.2. Metrics (Prometheus)
| Метрика | Тип | Описание |
|---|
grpc_server_started_total | counter | Всего вызовов (по методу, статусу). |
grpc_server_handled_total | counter | Завершённых вызовов. |
grpc_server_msg_received_total | counter | Принятых сообщений (streaming). |
grpc_server_msg_sent_total | counter | Отправленных сообщений. |
grpc_server_handling_seconds | histogram | Время обработки (P50, P95, P99). |
Экспорт в .NET:
services.AddOpenTelemetry()
.WithMetrics(builder => builder
.AddMeter("Grpc.AspNetCore.Server")
.AddPrometheusExporter());
app.MapPrometheusScrapingEndpoint("/metrics");
20.3. Логгирование
| Уровень | Что логировать | Формат |
|---|
| DEBUG | Входящий/исходящий запрос (для отладки) | Structured: { method: "UserService.GetUser", request: {...}, duration_ms: 12 } |
| INFO | Завершённые unary-вызовы | { method, status, duration_ms, user_id } |
| WARN | DEADLINE_EXCEEDED, UNAVAILABLE (не retryable) | { method, status, attempt } |
| ERROR | INTERNAL, UNKNOWN, DATA_LOSS | { method, status, stack_trace } |
⚠️ Никогда не логировать:
metadata (может содержать токены);
- поля с
field_behavior = INPUT_ONLY / SENSITIVE;
- содержимое
bytes, Any, Struct без маскировки.
21. Развёртывание и инфраструктура
21.1. Прокси и шлюзы
| Компонент | Назначение | Поддержка gRPC |
|---|
| Envoy | L4/L7 proxy, rate limiting, retries | Полная: transcoding (REST → gRPC), health checks (/grpc.health.v1.Health/Check), stats. |
| grpc-gateway | Генерация REST-прокси из .proto | Unary ↔ REST, streaming → SSE/WebSocket (ограниченно). |
| nginx | Ingress | Поддержка gRPC с 1.13.10+: grpc_pass grpc://backend; |
| Linkerd / Istio | Service mesh | Автоматический mTLS, retries, timeouts, circuit breaking. |
Пример Envoy config (transcoding):
routes:
- match: { prefix: "/v1/users/" }
route: { cluster: user_service }
typed_per_filter_config:
envoy.filters.http.grpc_json_transcoder:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
proto_descriptor: "/data/user_service.pb"
services: ["example.v1.UserService"]
print_options: { always_print_primitive_fields: true }
21.2. Health checks
Стандартный сервис grpc.health.v1.Health:
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
Check: unary, для readiness/liveness probes.
Watch: streaming, для клиентов, отслеживающих состояние.
🔹 Kubernetes:
livenessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:50051"]
readinessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:50051", "-service=UserService"]
21.3. CI/CD и проверка совместимости
| Инструмент | Команда | Эффект |
|---|
buf breaking | buf breaking --against '.git#branch=main' | Проверяет, не сломана ли совместимость с main. |
buf lint | buf lint | Соблюдение стиля (AIP, custom rules). |
protolock | protolock status | Legacy-альтернатива buf breaking. |
22. Миграция с REST/SOAP и гибридные API
22.1. Стратегии миграции
| Этап | Действие |
|---|
| 1. Dual API | Одновременная поддержка REST и gRPC (например, через grpc-gateway). |
| 2. Клиентская либа | SDK с unified API (.GetUser(id) → gRPC или REST по флагу). |
| 3. Внутреннее gRPC | Внешний REST, внутренние вызовы — gRPC. |
| 4. Полный переход | Только gRPC + gRPC-Web для фронтенда. |
22.2. Преимущества гибридного подхода
- Постепенный переход без downtime.
- Возможность оценить latency/bandwidth/нагрузку.
- Поддержка legacy-клиентов (mobile SDK v1).
22.3. Проблемы
- Дублирование логики (валидация, авторизация).
- Сложность поддержки двух схем (OpenAPI +
.proto).
- Разный набор фич (например, streaming недоступен в REST).
🔹 Решение: единственный источник истины — .proto. OpenAPI генерируется из него (protoc-gen-openapiv2).
23. Анти-паттерны и частые ошибки
| Проблема | Последствие | Исправление |
|---|
required в proto3 | Синтаксическая ошибка | Использовать optional + валидацию. |
| Field number reuse | Нарушение wire-совместимости | Всегда reserved старые номера. |
Отсутствие request_id | Невозможность идемпотентности | Добавить string request_id = 999 во все Request. |
| Большое сообщение (>4 MB) | RESOURCE_EXHAUSTED | Разбивать на stream, увеличивать MaxReceiveMessageSize. |
| Нет keepalive | Разрыв соединений прокси | Настроить GRPC_ARG_KEEPALIVE_TIME_MS. |
| Синхронный streaming | Блокировка потока, низкая пропускная способность | Использовать async/await, backpressure. |
Логгирование metadata | Утечка токенов | Фильтровать authorization, x-api-key в логгере. |
Отсутствие FieldMask в Update | Перезапись всех полей | Требовать update_mask, использовать mergeFrom. |
| Смешивание proto2/proto3 | Неочевидные default-значения | Стандартизировать на proto3 (или edition 2023). |