УРОК 28 / 30 0%

gRPC + Protocol Buffers

🎯

Цель урока

Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.

🧠

Теория · для собеса

1 Что такое gRPC

gRPC — фреймворк для межсервисного общения, созданный Google.

  • Protocol Buffers (protobuf) — бинарный формат сериализации (в 3-10x компактнее JSON)
  • HTTP/2 — multiplexed, bidirectional streaming
  • Строгая типизация через .proto контракт
2 Когда gRPC, а когда REST

| | REST + JSON | gRPC + protobuf |

|---|---|---|

| Когда | Публичный API, фронтенд | Микросервисы, mobile backend |

| Размер | Большой (JSON) | Маленький (binary) |

| Скорость | Средняя | Высокая |

| Типизация | Опционально (OpenAPI) | Обязательно (.proto) |

| Читаемость | Да (curl, Postman) | Нет (нужен grpcurl) |

| Streaming | Только WebSocket / SSE | Нативно (4 типа) |

3 4 типа gRPC вызовов
  1. Unary — клиент отправляет 1 запрос, получает 1 ответ (как REST)
  2. Server streaming — клиент отправляет 1, получает stream
  3. Client streaming — клиент отправляет stream, получает 1
  4. Bidirectional streaming — оба стримят

---

💻

Практика: улучшаем CRUD

  1. 1
    Зависимости
    
    plugins {
        id("com.google.protobuf") version "0.9.4"
    }
    
    dependencies {
        implementation("io.grpc:grpc-netty-shaded:1.66.0")
        implementation("io.grpc:grpc-protobuf:1.66.0")
        implementation("io.grpc:grpc-stub:1.66.0")
        compileOnly("org.apache.tomcat:annotations-api:6.0.53")  // для @Generated
    }
    
  2. 2
    src/main/proto/task.proto
    
    syntax = "proto3";
    
    option java_package = "com.taskflow.grpc";
    option java_multiple_files = true;
    
    service TaskService {
        rpc GetTask (TaskRequest) returns (TaskResponse);
        rpc ListTasks (ListTasksRequest) returns (ListTasksResponse);
        rpc CreateTask (CreateTaskRequest) returns (TaskResponse);
        rpc StreamTasks (StreamTasksRequest) returns (stream TaskResponse);  // server streaming
    }
    
    message TaskRequest {
        int64 id = 1;
    }
    
    message ListTasksRequest {
        int32 page = 1;
        int32 size = 2;
    }
    
    message ListTasksResponse {
        repeated TaskResponse tasks = 1;
    }
    
    message CreateTaskRequest {
        string title = 1;
        string description = 2;
        bool done = 3;
    }
    
    message StreamTasksRequest {
        string owner_username = 1;
    }
    
    message TaskResponse {
        int64 id = 1;
        string title = 2;
        string description = 3;
        bool done = 4;
        string owner_username = 5;
        int64 created_at_ms = 6;  // epoch millis
    }
    
  3. 3
    Gradle protobuf
    
    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:3.25.5"
        }
        plugins {
            id("grpc") {
                artifact = "io.grpc:protoc-gen-grpc-java:1.66.0"
            }
        }
        generateProtoTasks {
            all().forEach { task ->
                task.plugins {
                    create("grpc") { /* options */ }
                }
            }
        }
    }
    
    sourceSets {
        main {
            java {
                srcDirs("build/generated/source/proto/main/grpc")
                srcDirs("build/generated/source/proto/main/java")
            }
        }
    }
    

    После ./gradlew build сгенерируются:

    • TaskServiceGrpc.java — базовый класс сервера
    • TaskRequest.java, TaskResponse.java — POJO
  4. 4
    grpc/TaskServiceImpl.java (сервер)
    
    package com.taskflow.grpc;
    
    import com.taskflow.model.Task;
    import com.taskflow.repository.TaskRepository;
    import io.grpc.stub.StreamObserver;
    import net.devh.boot.grpc.server.service.GrpcService;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import java.util.List;
    
    @GrpcService
    public class TaskServiceImpl extends TaskServiceGrpc.TaskServiceImplBase {
    
        @Autowired private TaskRepository taskRepository;
    
        @Override
        public void getTask(TaskRequest request, StreamObserver responseObserver) {
            Task task = taskRepository.findById(request.getId())
                .orElseThrow(() -> new RuntimeException("Not found"));
    
            TaskResponse response = TaskResponse.newBuilder()
                .setId(task.getId())
                .setTitle(task.getTitle())
                .setDescription(task.getDescription() != null ? task.getDescription() : "")
                .setDone(task.isDone())
                .setOwnerUsername(task.getOwner().getUsername())
                .setCreatedAtMs(task.getCreatedAt().toEpochMilli())
                .build();
    
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        }
    
        @Override
        public void listTasks(ListTasksRequest request, StreamObserver responseObserver) {
            List tasks = taskRepository.findAll();
            ListTasksResponse.Builder builder = ListTasksResponse.newBuilder();
            for (Task t : tasks) {
                builder.addTasks(toProto(t));
            }
            responseObserver.onNext(builder.build());
            responseObserver.onCompleted();
        }
    
        @Override
        public void streamTasks(StreamTasksRequest request, StreamObserver responseObserver) {
            // Server streaming
            List tasks = taskRepository.findByOwnerUsername(request.getOwnerUsername());
            for (Task t : tasks) {
                responseObserver.onNext(toProto(t));
                try { Thread.sleep(100); } catch (InterruptedException e) {}
            }
            responseObserver.onCompleted();
        }
    
        private TaskResponse toProto(Task t) {
            return TaskResponse.newBuilder()
                .setId(t.getId())
                .setTitle(t.getTitle())
                .setDescription(t.getDescription() != null ? t.getDescription() : "")
                .setDone(t.isDone())
                .setOwnerUsername(t.getOwner().getUsername())
                .setCreatedAtMs(t.getCreatedAt().toEpochMilli())
                .build();
        }
    }
    
  5. 5
    Клиент
    
    @Component
    public class TaskGrpcClient {
        private final TaskServiceGrpc.TaskServiceBlockingStub blockingStub;
    
        public TaskGrpcClient(@Value("${grpc.client.host:localhost}") String host,
                              @Value("${grpc.client.port:9090}") int port) {
            ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext()
                .build();
            this.blockingStub = TaskServiceGrpc.newBlockingStub(channel);
        }
    
        public TaskResponse getTask(Long id) {
            return blockingStub.getTask(TaskRequest.newBuilder().setId(id).build());
        }
    }
    
  6. 6
    application.yml
    
    grpc:
      server:
        port: 9090
    
  7. 7
    Тест через grpcurl
    
    # Установка
    brew install grpcurl
    
    # Unary call
    $ grpcurl -plaintext -d '{"id":1}' localhost:9090 taskflow.TaskService/GetTask
    
    # Server streaming
    $ grpcurl -plaintext -d '{"owner_username":"alice"}' localhost:9090 taskflow.TaskService/StreamTasks
    

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

Скорость: protobuf в 3-10x быстрее JSON. На high-RPS системах — заметно.
Типизация: контракт .proto — единый для всех языков (Java, Go, Python, Node).
Streaming: нативно, без WebSocket. Realtime данные, video, audio.
Полиглот: один .proto → сервер на Java, клиент на Go.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
.proto vs OpenAPI — чем отличаются контракты?

Разбор внутри: protobuf schema, codegen и REST-документация.

2
Что делает protoc и зачем нужны плагины?

Разбор внутри: компилятор proto и генерация Java/gRPC кода.

3
Почему gRPC использует HTTP/2?

Разбор внутри: multiplexing, header compression и streaming.

4
Почему gRPC неудобен для публичного browser API?

Разбор внутри: grpc-web, Envoy proxy и ограничения браузера.

5
Как дебажить gRPC-сервис?

Разбор внутри: grpcurl, interceptors и reflection.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 29

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 27
🎉
Новый тир
Новый тир достигнут!