Roman Kryvolapov Engineering Blog

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.GreeterGrpcKt
import greet.HelloReply
import greet.HelloRequest
import 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.GreeterGrpcKt
import greet.HelloRequest
import io.grpc.ManagedChannelBuilder
import 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 EDIT
package 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 EDIT
package 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()
}

Copyright: Roman Kryvolapov