gRPCでKotlinのTutorialをやってみた
はじめに
業務でgRPC
でAPIを開発することになりました。gRPC
自体は知っているのですが、実装したことが無かったので公式のtutorialをしてみて分からなかった部分を調べました。
レポジトリのクローン
$ git clone --depth 1 https://github.com/grpc/grpc-kotlin \
$ cd grpc-kotlin/examples
- clientとserverをコンパイル
$ ./gradlew installDist
実行
- serverを起動
$ ./server/build/install/server/bin/hello-world-server
- clientを起動
別terminalでclientを実行すると。。。
$ ./client/build/install/client/bin/hello-world-client
Received: Hello world
何が起きているのか
Tutorialの通りにやったので動くのは当たり前なのですが、実際にはどのように動いているのでしょう。
Server
サーバー側のコードは下記のコードです。
class HelloWorldServer(private val port: Int) {
val server: Server =
ServerBuilder
.forPort(port)
.addService(HelloWorldService())
.build()
fun start() {
server.start()
println("Server started, listening on $port")
Runtime.getRuntime().addShutdownHook(
Thread {
println("*** shutting down gRPC server since JVM is shutting down")
this@HelloWorldServer.stop()
println("*** server shut down")
},
)
}
private fun stop() {
server.shutdown()
}
fun blockUntilShutdown() {
server.awaitTermination()
}
internal class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
override suspend fun sayHello(request: HelloRequest) =
helloReply {
message = "Hello ${request.name}"
}
}
}
ここでいくつか分からないことが。
-
fun start()
の中の、
Runtime.getRuntime().addShutdownHook(
Thread {
println("*** shutting down gRPC server since JVM is shutting down")
this@HelloWorldServer.stop()
println("*** server shut down")
},
)
どうやら、ここでグレースフルシャットダウンをしているみたいです。
グレースフルシャットダウン:アプリをシャットダウンする時に、きちんとプロセスを管理し、決められた順番でシャットダウンすることで安全にアプリを停止させるシャットダウンの方法のこと
.addShutdownHook
でJVMがシャットダウンを始めた際に、このThreadが呼び出されます。
this@HelloWorldServer.stop()
はThread
の内部から、外部のクラスのインスタンスを参照するのに使われています。
-
fun blockUntilShutdown()
の中の、
server.awaitTermination()
awaitTermination()
はサーバーを起動させたままにしておくのではなく、アプリケーションが停止するのを防止します。サーバー自体は、自身のインスタンス(HelloWorldService)の実行を継続させながら、RPCをハンドリングします。
このコードがないと、プログラムがmain
処理の最後まで辿り着いて、プログラムが終了します。
消してみると、SIGINTしていないのに下記のような感じですぐに終了。
Server started, listening on 50051
*** shutting down gRPC server since JVM is shutting down
*** server shut down
Client
クライアント側のコードは下記のコードです。
class HelloWorldClient(private val channel: ManagedChannel) : Closeable {
private val stub: GreeterCoroutineStub = GreeterCoroutineStub(channel)
suspend fun greet(name: String) {
val request = helloRequest { this.name = name }
val response = stub.sayHello(request)
println("Received: ${response.message}")
}
override fun close() {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS)
}
}
suspend fun main(args: Array<String>) {
val port = System.getenv("PORT")?.toInt() ?: 50051
val channel = ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build()
val client = HelloWorldClient(channel)
val user = args.singleOrNull() ?: "world"
client.greet(user)
}
suspend fun greet(name: String) {
val request = helloRequest { this.name = name }
val response = stub.sayHello(request)
println("Received: ${response.message}")
}
このsuspend
でfun greet()
をcoroutine化しています。
HTTPリクエストは(CPUに比べ)時間がかかるので非同期処理にしています。
Client
とServer
で使われている2つの型(HelloRequest
, HelloReply
)はどこで定義されているのかと思ったのですが、proto
ファイルで定義された型を参照していますね。
val request = helloRequest { this.name = name }
internal class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
override suspend fun sayHello(request: HelloRequest) =
helloReply {
message = "Hello ${request.name}"
}
}
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
終わりに
Tutorialをやってみて、実装する手順が大分明確になりました。
Discussion