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+ езика.
Contract-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 (копираме го и в двата модула)

Protobuf
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

Kotlin
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)

Kotlin
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

Kotlin
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

Kotlin
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)

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

Kotlin
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)

Kotlin
// 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