Protocol Buffers: сердце gRPC
Если gRPC — это двигатель взаимодействия сервисов, то Protocol Buffers (protobuf) — это его сердце.
Именно protobuf определяет, как описываются данные, как они сериализуются, и как из одной схемы генерируются типобезопасные классы для разных языков.
Чтобы по-настоящему понимать gRPC, нужно уверенно работать с .proto-файлами.
1. Что такое .proto файл
.proto — это файл описания структуры данных и интерфейсов (API).
Он играет сразу три роли:
Документирует контракт между клиентом и сервером (описывает, какие методы и какие данные доступны).
Генерирует код для разных языков с помощью protoc (компилятора Protocol Buffers).
Определяет схему сериализации — то, как объекты превращаются в байты и обратно.
Фактически .proto — это единый источник правды для вашего API.
2. Базовая структура .proto файла
Пример простого файла:
3. Ключевые элементы .proto
3.1. syntax
Первая строка файла:
Обязательно указывает версию синтаксиса.
На практике используется только proto3, потому что она проще, строже типизирована и лучше поддерживается в gRPC.
3.2. package
Задает логическое пространство имён, чтобы избежать конфликтов:
В Java и других языках это превращается в пакеты/модули.
3.3. option
Позволяет задавать настройки генерации кода, например:
Без этого весь код попадёт в один файл, что неудобно для больших схем.
3.4. message — описание структуры данных
message — это аналог класса в объектно-ориентированных языках.
Каждое поле внутри него — это свойство (переменная), которое сериализуется в бинарный поток.
Пример:
3.5. enum — перечисление значений
enum — это список допустимых констант.
Значение 0 обязательно — это значение по умолчанию.
При сериализации хранится не текстовое имя ("ACTIVE"), а его числовое значение (0), что делает protobuf компактным.
3.6. service — описание API
service определяет набор удалённых методов, которые сервер предоставляет клиенту.
Это аналог интерфейса в Java:
Каждый rpc определяет:
имя метода (BuyCar),
входной тип (CarRequest),
выходной тип (CarResponse).
4. Типы данных в Protocol Buffers
Protobuf поддерживает ограниченный, но универсальный набор типов.
Некоторые часто используемые:
string - Текст
bool - Логическое значение
int32, int64 - Целые числа
float, double - Числа с плавающей точкой
bytes - Массив байтов
repeated - Массив
map<key, value> - Словарь
Пример:
#Java #middle #gRPC #proto
Если gRPC — это двигатель взаимодействия сервисов, то Protocol Buffers (protobuf) — это его сердце.
Именно protobuf определяет, как описываются данные, как они сериализуются, и как из одной схемы генерируются типобезопасные классы для разных языков.
Чтобы по-настоящему понимать gRPC, нужно уверенно работать с .proto-файлами.
1. Что такое .proto файл
.proto — это файл описания структуры данных и интерфейсов (API).
Он играет сразу три роли:
Документирует контракт между клиентом и сервером (описывает, какие методы и какие данные доступны).
Генерирует код для разных языков с помощью protoc (компилятора Protocol Buffers).
Определяет схему сериализации — то, как объекты превращаются в байты и обратно.
Фактически .proto — это единый источник правды для вашего API.
2. Базовая структура .proto файла
Пример простого файла:
syntax = "proto3";
package car;
option java_multiple_files = true;
option java_package = "com.example.car";
option java_outer_classname = "CarProto";
// Определение сообщений
message Car {
string model = 1;
int32 year = 2;
CarStatus status = 3;
}
// Перечисление (enum)
enum CarStatus {
ACTIVE = 0;
INACTIVE = 1;
}
// Определение сервиса
service CarService {
rpc BuyCar (CarRequest) returns (CarResponse);
}
message CarRequest {
string model = 1;
}
message CarResponse {
string confirmation = 1;
}
3. Ключевые элементы .proto
3.1. syntax
Первая строка файла:
syntax = "proto3";
Обязательно указывает версию синтаксиса.
На практике используется только proto3, потому что она проще, строже типизирована и лучше поддерживается в gRPC.
3.2. package
Задает логическое пространство имён, чтобы избежать конфликтов:
package car;
В Java и других языках это превращается в пакеты/модули.
3.3. option
Позволяет задавать настройки генерации кода, например:
option java_package = "com.example.car";
option java_multiple_files = true;
option java_outer_classname = "CarProto";
Без этого весь код попадёт в один файл, что неудобно для больших схем.
3.4. message — описание структуры данных
message — это аналог класса в объектно-ориентированных языках.
Каждое поле внутри него — это свойство (переменная), которое сериализуется в бинарный поток.
Пример:
message User {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
}
string name = 1; — поле с типом string и номером 1.
int32 age = 2; — целочисленное поле.
repeated string hobbies = 3; — массив строк.
Важно: номер поля (= 1, = 2, = 3) — это не просто индекс. Это ключ в бинарной сериализации, который должен быть уникален и неизменен.3.5. enum — перечисление значений
enum — это список допустимых констант.
enum CarStatus {
ACTIVE = 0;
INACTIVE = 1;
SOLD = 2;
}Значение 0 обязательно — это значение по умолчанию.
При сериализации хранится не текстовое имя ("ACTIVE"), а его числовое значение (0), что делает protobuf компактным.
3.6. service — описание API
service определяет набор удалённых методов, которые сервер предоставляет клиенту.
Это аналог интерфейса в Java:
service CarService {
rpc BuyCar (CarRequest) returns (CarResponse);
rpc ListCars (Empty) returns (CarList);
}Каждый rpc определяет:
имя метода (BuyCar),
входной тип (CarRequest),
выходной тип (CarResponse).
4. Типы данных в Protocol Buffers
Protobuf поддерживает ограниченный, но универсальный набор типов.
Некоторые часто используемые:
string - Текст
bool - Логическое значение
int32, int64 - Целые числа
float, double - Числа с плавающей точкой
bytes - Массив байтов
repeated - Массив
map<key, value> - Словарь
Пример:
message Garage {
map<string, Car> cars = 1;
}#Java #middle #gRPC #proto
👍2
5. Нумерация полей — почему это критично
Каждое поле имеет свой уникальный номер — это его идентификатор в бинарном потоке.
Если поменять номера, клиент и сервер перестанут понимать друг друга.
Например, если у старой версии клиента year = 2, а у новой year = 3, при сериализации они будут читать разные данные.
6. Почему важно резервировать поля
Когда вы удаляете или переименовываете поле, нельзя просто убрать строку — нужно зарезервировать номер и имя.
Пример:
Это предотвращает случайное переиспользование старого номера под другое поле, что может привести к неправильной интерпретации данных.
7. Эволюция и миграция схем (Schema Evolution)
Protobuf специально спроектирован так, чтобы позволять обновлять схемы без поломки совместимости.
Главное — соблюдать несколько правил.
Что можно делать безопасно:
Добавлять новые поля с новыми номерами.
Удалять поля (с их резервированием).
Изменять имя поля (номер должен остаться прежним).
Изменять порядок полей — не влияет на сериализацию.
Что делать нельзя:
Менять тип поля (например, int32 → string).
Менять номер поля.
Удалять поле без reserved.
Пример миграции
Старая версия:
Новая версия:
Старый клиент, который не знает про email, просто проигнорирует это поле.
А новый клиент не столкнётся с конфликтом, потому что старый 2 зарезервирован.
8. Компиляция .proto файла и генерация кода
protoc — компилятор, который читает .proto и создаёт Java-классы.
Пример команды:
Результат:
Для каждого message создаются классы с Builder-паттерном.
Для service создаются классы CarServiceGrpc, CarServiceImplBase, CarServiceStub.
9. Пример полного цикла
Файл car.proto:
Сгенерированный код в Java (упрощённо):
Серверная реализация:
#Java #middle #gRPC #proto
Каждое поле имеет свой уникальный номер — это его идентификатор в бинарном потоке.
message Car {
string model = 1;
int32 year = 2;
}Если поменять номера, клиент и сервер перестанут понимать друг друга.
Например, если у старой версии клиента year = 2, а у новой year = 3, при сериализации они будут читать разные данные.
6. Почему важно резервировать поля
Когда вы удаляете или переименовываете поле, нельзя просто убрать строку — нужно зарезервировать номер и имя.
Пример:
message Car {
string model = 1;
reserved 2; // резервируем номер
reserved "status_old"; // резервируем имя
}Это предотвращает случайное переиспользование старого номера под другое поле, что может привести к неправильной интерпретации данных.
7. Эволюция и миграция схем (Schema Evolution)
Protobuf специально спроектирован так, чтобы позволять обновлять схемы без поломки совместимости.
Главное — соблюдать несколько правил.
Что можно делать безопасно:
Добавлять новые поля с новыми номерами.
Удалять поля (с их резервированием).
Изменять имя поля (номер должен остаться прежним).
Изменять порядок полей — не влияет на сериализацию.
Что делать нельзя:
Менять тип поля (например, int32 → string).
Менять номер поля.
Удалять поле без reserved.
Пример миграции
Старая версия:
message User {
string name = 1;
int32 age = 2;
}Новая версия:
message User {
string name = 1;
reserved 2;
string email = 3;
}Старый клиент, который не знает про email, просто проигнорирует это поле.
А новый клиент не столкнётся с конфликтом, потому что старый 2 зарезервирован.
8. Компиляция .proto файла и генерация кода
protoc — компилятор, который читает .proto и создаёт Java-классы.
Пример команды:
protoc \
--java_out=./build/generated \
--grpc-java_out=./build/generated \
proto/car.proto
Результат:
Для каждого message создаются классы с Builder-паттерном.
Для service создаются классы CarServiceGrpc, CarServiceImplBase, CarServiceStub.
9. Пример полного цикла
Файл car.proto:
syntax = "proto3";
service CarService {
rpc BuyCar (CarRequest) returns (CarResponse);
}
message CarRequest {
string model = 1;
int32 budget = 2;
}
message CarResponse {
string message = 1;
}
Сгенерированный код в Java (упрощённо):
// Отправитель (клиент)
CarRequest request = CarRequest.newBuilder()
.setModel("BMW")
.setBudget(20000)
.build();
CarResponse response = stub.buyCar(request);
System.out.println(response.getMessage());
Серверная реализация:
public class CarServiceImpl extends CarServiceGrpc.CarServiceImplBase {
@Override
public void buyCar(CarRequest request, StreamObserver<CarResponse> responseObserver) {
String msg = "Car purchased: " + request.getModel();
CarResponse response = CarResponse.newBuilder().setMessage(msg).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}#Java #middle #gRPC #proto
👍3
Типы RPC в gRPC
Одно из ключевых преимуществ gRPC — это гибкость модели обмена данными.
REST традиционно работает в стиле “один запрос — один ответ”.
gRPC, в отличие от него, поддерживает четыре типа взаимодействия, и каждый из них решает свою задачу.
1. Unary RPC (один запрос — один ответ)
Это самый простой и самый распространённый тип — аналог классического REST-вызова.
Клиент отправляет один запрос, сервер обрабатывает его и возвращает один ответ.
Клиент → (один запрос) → Сервер
Сервер → (один ответ) → Клиент
Пример .proto
Сервер (Java)
Клиент
Где применяется:
CRUD-операции (создание, получение, обновление, удаление).
Любые точечные вызовы, где не требуется поток данных.
По сути: это "REST, но бинарный, типобезопасный и в 10 раз быстрее".
2. Server Streaming RPC (поток ответов от сервера)
В этом типе клиент делает один запрос, а сервер возвращает несколько ответов последовательно — поток сообщений.
Клиент → (один запрос) → Сервер
Сервер → (много ответов в потоке) → Клиент
Сеанс продолжается, пока сервер не закончит отправку данных.
Пример .proto
Сервер (Java)
Клиент
Где применяется:
Поток обновлений или уведомлений.
Стриминг данных (например, список записей, логи, результаты аналитики).
Долгие вычисления, когда сервер постепенно отдаёт результаты.
Пример из реального мира:
Сервер передаёт клиенту “живой” поток котировок акций или данных из IoT-устройств.
#Java #middle #gRPC #proto
Одно из ключевых преимуществ gRPC — это гибкость модели обмена данными.
REST традиционно работает в стиле “один запрос — один ответ”.
gRPC, в отличие от него, поддерживает четыре типа взаимодействия, и каждый из них решает свою задачу.
1. Unary RPC (один запрос — один ответ)
Это самый простой и самый распространённый тип — аналог классического REST-вызова.
Клиент отправляет один запрос, сервер обрабатывает его и возвращает один ответ.
Клиент → (один запрос) → Сервер
Сервер → (один ответ) → Клиент
Пример .proto
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}
message UserRequest {
int32 id = 1;
}
message UserResponse {
string name = 1;
int32 age = 2;
}Сервер (Java)
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
UserResponse response = UserResponse.newBuilder()
.setName("Alice")
.setAge(30)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}Клиент
UserResponse response = stub.getUser(
UserRequest.newBuilder().setId(1).build()
);
System.out.println(response.getName());
Где применяется:
CRUD-операции (создание, получение, обновление, удаление).
Любые точечные вызовы, где не требуется поток данных.
По сути: это "REST, но бинарный, типобезопасный и в 10 раз быстрее".
2. Server Streaming RPC (поток ответов от сервера)
В этом типе клиент делает один запрос, а сервер возвращает несколько ответов последовательно — поток сообщений.
Клиент → (один запрос) → Сервер
Сервер → (много ответов в потоке) → Клиент
Сеанс продолжается, пока сервер не закончит отправку данных.
Пример .proto
service OrderService {
rpc ListOrders (OrdersRequest) returns (stream Order);
}
message OrdersRequest {
string user = 1;
}
message Order {
string id = 1;
string product = 2;
}Сервер (Java)
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void listOrders(OrdersRequest request, StreamObserver<Order> responseObserver) {
for (int i = 1; i <= 3; i++) {
Order order = Order.newBuilder()
.setId("ORD-" + i)
.setProduct("Product " + i)
.build();
responseObserver.onNext(order);
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
}
responseObserver.onCompleted();
}
}Клиент
stub.listOrders(OrdersRequest.newBuilder().setUser("Bob").build())
.forEachRemaining(order -> System.out.println(order.getProduct()));Где применяется:
Поток обновлений или уведомлений.
Стриминг данных (например, список записей, логи, результаты аналитики).
Долгие вычисления, когда сервер постепенно отдаёт результаты.
Пример из реального мира:
Сервер передаёт клиенту “живой” поток котировок акций или данных из IoT-устройств.
#Java #middle #gRPC #proto
👍2
3. Client Streaming RPC (поток запросов от клиента)
Теперь наоборот — клиент отправляет поток запросов, а сервер отвечает одним итоговым сообщением.
Клиент → (много запросов) → Сервер
Сервер → (один ответ) → Клиент
Это удобно, когда клиенту нужно собрать несколько событий или пакетов данных и отправить их вместе.
Пример .proto
Сервер (Java)
Клиент
Где применяется:
Отправка файлов по частям.
Отчёты, собираемые из нескольких частей.
Потоковое логирование от клиента на сервер.
#Java #middle #gRPC #proto
Теперь наоборот — клиент отправляет поток запросов, а сервер отвечает одним итоговым сообщением.
Клиент → (много запросов) → Сервер
Сервер → (один ответ) → Клиент
Это удобно, когда клиенту нужно собрать несколько событий или пакетов данных и отправить их вместе.
Пример .proto
service UploadService {
rpc UploadPhotos (stream PhotoChunk) returns (UploadStatus);
}
message PhotoChunk {
bytes content = 1;
}
message UploadStatus {
string message = 1;
}Сервер (Java)
public class UploadServiceImpl extends UploadServiceGrpc.UploadServiceImplBase {
@Override
public StreamObserver<PhotoChunk> uploadPhotos(StreamObserver<UploadStatus> responseObserver) {
return new StreamObserver<PhotoChunk>() {
int totalBytes = 0;
@Override
public void onNext(PhotoChunk chunk) {
totalBytes += chunk.getContent().size();
}
@Override
public void onError(Throwable t) {
System.err.println("Upload failed: " + t.getMessage());
}
@Override
public void onCompleted() {
UploadStatus status = UploadStatus.newBuilder()
.setMessage("Uploaded " + totalBytes + " bytes")
.build();
responseObserver.onNext(status);
responseObserver.onCompleted();
}
};
}
}Клиент
StreamObserver<PhotoChunk> requestObserver = asyncStub.uploadPhotos(
new StreamObserver<UploadStatus>() {
@Override
public void onNext(UploadStatus status) {
System.out.println(status.getMessage());
}
@Override
public void onError(Throwable t) {}
@Override
public void onCompleted() {}
}
);
// Отправляем несколько чанков
requestObserver.onNext(PhotoChunk.newBuilder().setContent(ByteString.copyFrom(new byte[1000])).build());
requestObserver.onNext(PhotoChunk.newBuilder().setContent(ByteString.copyFrom(new byte[500])).build());
requestObserver.onCompleted();
Где применяется:
Отправка файлов по частям.
Отчёты, собираемые из нескольких частей.
Потоковое логирование от клиента на сервер.
#Java #middle #gRPC #proto
👍1
4. Bidirectional Streaming RPC (двунаправленный поток)
Самый мощный и сложный тип.
Клиент и сервер одновременно отправляют данные потоками.
Они не ждут завершения друг друга — общение идёт асинхронно в обе стороны.
Клиент ⇄ (двунаправленный поток) ⇄ Сервер
Пример .proto
Сервер (Java)
Клиент
Где применяется:
Чаты и видеоконференции.
Онлайн-игры и взаимодействие в реальном времени.
Телеметрия, двунаправленные датчики, IoT.
Главная особенность: оба канала открыты, и клиент, и сервер могут посылать данные независимо друг от друга.
5. Под капотом
Все типы RPC работают поверх HTTP/2, где каждый поток — это отдельный канал в рамках одного TCP-соединения.
gRPC использует этот механизм для организации стримов.
По сути, StreamObserver в Java — это высокоуровневая абстракция над HTTP/2-стримом, обеспечивающая асинхронность и реактивное взаимодействие.
#Java #middle #gRPC #proto
Самый мощный и сложный тип.
Клиент и сервер одновременно отправляют данные потоками.
Они не ждут завершения друг друга — общение идёт асинхронно в обе стороны.
Клиент ⇄ (двунаправленный поток) ⇄ Сервер
Пример .proto
service ChatService {
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user = 1;
string text = 2;
}Сервер (Java)
public class ChatServiceImpl extends ChatServiceGrpc.ChatServiceImplBase {
@Override
public StreamObserver<ChatMessage> chat(StreamObserver<ChatMessage> responseObserver) {
return new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage message) {
// Эхо-сообщение обратно клиенту
ChatMessage reply = ChatMessage.newBuilder()
.setUser("Server")
.setText("Echo: " + message.getText())
.build();
responseObserver.onNext(reply);
}
@Override
public void onError(Throwable t) {
System.err.println("Error: " + t.getMessage());
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
}Клиент
StreamObserver<ChatMessage> requestObserver = asyncStub.chat(
new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage value) {
System.out.println(value.getUser() + ": " + value.getText());
}
@Override
public void onError(Throwable t) {}
@Override
public void onCompleted() {}
}
);
requestObserver.onNext(ChatMessage.newBuilder().setUser("Client").setText("Hello!").build());
requestObserver.onNext(ChatMessage.newBuilder().setUser("Client").setText("How are you?").build());
requestObserver.onCompleted();
Где применяется:
Чаты и видеоконференции.
Онлайн-игры и взаимодействие в реальном времени.
Телеметрия, двунаправленные датчики, IoT.
Главная особенность: оба канала открыты, и клиент, и сервер могут посылать данные независимо друг от друга.
5. Под капотом
Все типы RPC работают поверх HTTP/2, где каждый поток — это отдельный канал в рамках одного TCP-соединения.
gRPC использует этот механизм для организации стримов.
По сути, StreamObserver в Java — это высокоуровневая абстракция над HTTP/2-стримом, обеспечивающая асинхронность и реактивное взаимодействие.
#Java #middle #gRPC #proto
👍2