gRPC — Як це працює
Що таке gRPC і Protocol Buffers
gRPC (Google Remote Procedure Calls):
система віддаленого виклику процедур (RPC) з відкритим кодом, спочатку розроблена в Google у 2015 році. Як транспорт використовується HTTP/2, як мова опису інтерфейсу — Protocol Buffers. gRPC надає такі функції як автентифікація, двонаправлена потокова передача та керування потоком, блокуючі або неблокуючі прив'язки, а також скасування й тайм-аути. Генерує кросплатформні прив'язки клієнта і сервера для багатьох мов. Найчастіше використовується для підключення служб у мікросервісному стилі архітектури та підключення мобільних пристроїв і браузерних клієнтів до серверних служб.
Protocol Buffers:
протокол серіалізації (передачі) структурованих даних, запропонований Google як ефективна бінарна альтернатива текстовому формату XML. Розробники повідомляють, що Protocol Buffers простіший, компактніший і швидший, ніж XML, оскільки здійснюється передача бінарних даних, оптимізованих під мінімальний розмір повідомлення.
Чому gRPC кращий за REST для внутрішніх сервісів
Чому gRPC кращий за REST для внутрішніх сервісів:
HTTP/2 бінарний формат (protobuf) → менше трафіку і CPU.
Мультиплексування в одному TCP-з'єднанні → менше з'єднань, нижча латентність.
Односторонні, серверні та двосторонні стріми без long-polling.
Згенеровані типобезпечні клієнти на 15+ мовах.
Контракт-first: зміни схеми виявляються на етапі компіляції.
Вбудовані deadline, retry, load-balancing, TLS.
Спостережуваність: метадані (headers/trailers) підходять для трасувань і метрик.
Просте винесення назовні: можна покласти grpc-gateway перед сервісом і отримати REST/JSON без переписування коду.
Приклад: створення grpc-server і grpc-client
Для прикладу зробимо grpc-server і grpc-client
Структура проєкту
simple-grpc│ settings.gradle.kts├─grpc-server│ ├─build.gradle.kts│ ├─src/main/proto/hello.proto│ └─src/main/kotlin/com/example/grpc/Server.kt└─grpc-client ├─build.gradle.kts ├─src/main/proto/hello.proto └─src/main/kotlin/com/example/grpc/Client.ktУ файлі *.proto записано, які повідомлення й методи розуміє сервіс.
Приблизно як договір між клієнтом і сервером:
service:
перелічує доступні операції (RPC-методи).
message:
описує поля запитів і відповідей (тип, ім'я, порядковий номер).
Версія файлу лежить у VCS; обидві сторони генерують код із цієї однієї схеми й отримують строго типобезпечні класи.
Навіщо потрібен:
Єдине джерело правди – ні клієнт, ні сервер не можуть «розсинхронізуватися» за форматом.
Автогенерація – писати треба лише логіку, а не серіалізацію/десеріалізацію.
Перевірка на етапі збірки – змінив поле в контракті компілятор одразу покаже, де в коді потрібно поправити.
багатомовність – з одного й того самого proto можна за пару команд отримати клієнт на Kotlin, Go, Python тощо.
*.proto описує API одного разу; все інше генерується.
Після генерації з'являться GreeterGrpcKt.GreeterCoroutineImplBase (для сервера) і GreeterCoroutineStub (для клієнта).
src/main/proto/hello.proto (копіюємо в обидва модулі)
syntax = "proto3";package greet;
service Greeter { rpc SayHello (HelloRequest) returns (HelloReply);}
message HelloRequest { string name = 1; }message HelloReply { string message = 1; }grpc-server/build.gradle.kts
plugins { kotlin("jvm") version "2.2.0" id("application") id("com.google.protobuf") version "0.9.4"}
repositories { mavenCentral() }
dependencies { implementation("io.grpc:grpc-kotlin-stub:1.4.3") implementation("io.grpc:grpc-protobuf:1.73.0") implementation("io.grpc:grpc-stub:1.73.0") implementation("com.google.protobuf:protobuf-kotlin:4.31.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.21") runtimeOnly("io.grpc:grpc-netty-shaded:1.73.0")}
application { mainClass.set("com.example.grpc.ServerKt") }
protobuf { protoc { artifact = "com.google.protobuf:protoc:4.31.1" } plugins { id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.73.0" } id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.3:jdk8@jar" } } generateProtoTasks.all().forEach { t -> t.plugins { id("grpc"); id("grpckt") } }}grpc-client/build.gradle.kts (відрізняється тільки mainClass)
plugins { kotlin("jvm") version "2.2.0" id("application") id("com.google.protobuf") version "0.9.4"}
repositories { mavenCentral() }
dependencies { implementation("io.grpc:grpc-kotlin-stub:1.4.3") implementation("io.grpc:grpc-protobuf:1.73.0") implementation("io.grpc:grpc-stub:1.73.0") implementation("com.google.protobuf:protobuf-kotlin:4.31.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.21") runtimeOnly("io.grpc:grpc-netty-shaded:1.73.0")}
application { mainClass.set("com.example.grpc.ClientKt") }
protobuf { protoc { artifact = "com.google.protobuf:protoc:4.31.1" } plugins { id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.73.0" } id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.3:jdk8@jar" } } generateProtoTasks.all().forEach { t -> t.plugins { id("grpc"); id("grpckt") } }}Server.kt
package com.example.grpc
import greet.GreeterGrpcKtimport greet.HelloReplyimport greet.HelloRequestimport io.grpc.ServerBuilder
class GreeterService : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply = HelloReply.newBuilder() .setMessage("Hello, ${request.name}") .build()}
fun main() { val server = ServerBuilder .forPort(9090) .addService(GreeterService()) .build() .start() println("gRPC server on 9090") Runtime.getRuntime().addShutdownHook(Thread { server.shutdown() }) server.awaitTermination()}Client.kt
package com.example.grpc
import greet.GreeterGrpcKtimport greet.HelloRequestimport io.grpc.ManagedChannelBuilderimport kotlinx.coroutines.runBlocking
fun main() = runBlocking { val channel = ManagedChannelBuilder .forAddress("localhost", 9090) .usePlaintext() .build() val stub = GreeterGrpcKt.GreeterCoroutineStub(channel) val reply = stub.sayHello( HelloRequest.newBuilder().setName("Roman").build() ) println(reply.message) channel.shutdownNow()}Після запуску клієнт виведе Hello, Roman, підтверджуючи роботу gRPC-виклику.
Приклади згенерованих класів
HelloRequestKt.kt, HelloReplyKt.kt:
Kotlin-версії повідомлень; у кожного є Builder, методи parseFrom, toByteArray.
GreeterGrpcKt.kt містить два класи:
GreeterCoroutineImplBase:
базовий abstract-клас, від якого успадковується сервер; потрібно реалізувати sayHello.
GreeterCoroutineStub:
типобезпечний клієнт; метод sayHello - звичайний suspend.
GreeterGrpc.java (не показаний):
те саме, але без корутин; залишається, якщо потрібен Java-код.
HelloRequestKt.kt (protobuf-kotlin)
// AUTO-GENERATED BY PROTOC, DO NOT EDITpackage greet
import com.google.protobuf.kotlin.*
@OptIn(ProtobufSyntaxSupport::class)class HelloRequest private constructor( _name: String = "") : com.google.protobuf.GeneratedMessageLite< HelloRequest, HelloRequest.Builder>(DEFAULT_INSTANCE) {
var name: String = _name private set
// builder pattern class Builder : com.google.protobuf.GeneratedMessageLite.Builder< HelloRequest, Builder>(DEFAULT_INSTANCE) { fun setName(value: String): Builder = apply { instance.name = value } fun build(): HelloRequest = instance }
companion object { private val DEFAULT_INSTANCE = HelloRequest() fun newBuilder(): Builder = Builder() @JvmStatic fun parseFrom(bytes: ByteArray) = DEFAULT_INSTANCE.parseFrom(bytes) }}HelloReplyKt.kt
package greet
import com.google.protobuf.kotlin.*
class HelloReply private constructor( _message: String = "") : com.google.protobuf.GeneratedMessageLite< HelloReply, HelloReply.Builder>(DEFAULT_INSTANCE) {
var message: String = _message private set
class Builder : com.google.protobuf.GeneratedMessageLite.Builder< HelloReply, Builder>(DEFAULT_INSTANCE) { fun setMessage(v: String): Builder = apply { instance.message = v } fun build(): HelloReply = instance }
companion object { private val DEFAULT_INSTANCE = HelloReply() fun newBuilder(): Builder = Builder() }}GreeterGrpcKt.kt (gRPC-Kotlin plugin)
// AUTO-GENERATED BY PROTOC, DO NOT EDITpackage greet
import io.grpc.kotlin.*import kotlinx.coroutines.flow.Flow
object GreeterGrpcKt { const val SERVICE_NAME: String = "greet.Greeter"
abstract class GreeterCoroutineImplBase( coroutineContext: kotlin.coroutines.CoroutineContext = kotlinx.coroutines.Dispatchers.Default ) : AbstractCoroutineServerImpl(coroutineContext) {
open suspend fun sayHello(request: HelloRequest): HelloReply = throw io.grpc.Status.UNIMPLEMENTED.asRuntimeException()
final override fun bindService() = service { unaryRpc( method = METHOD_SAY_HELLO, implementation = ::sayHello ) } }
class GreeterCoroutineStub private constructor( channel: io.grpc.Channel, callOptions: io.grpc.CallOptions ) : AbstractCoroutineStub<GreeterCoroutineStub>(channel, callOptions) {
suspend fun sayHello(request: HelloRequest): HelloReply = unaryRpc( channel, METHOD_SAY_HELLO, request, callOptions, HelloReply.getDefaultInstance() )
override fun build(channel: io.grpc.Channel, callOptions: io.grpc.CallOptions) = GreeterCoroutineStub(channel, callOptions) }
private val METHOD_SAY_HELLO = io.grpc.MethodDescriptor.newBuilder<HelloRequest, HelloReply>() .setType(io.grpc.MethodDescriptor.MethodType.UNARY) .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName( SERVICE_NAME, "SayHello")) .setRequestMarshaller(protoLiteRequestMarshaller(HelloRequest.getDefaultInstance())) .setResponseMarshaller(protoLiteResponseMarshaller(HelloReply.getDefaultInstance())) .build()}