💡

gRPCでKotlinのTutorialをやってみた

2024/04/08に公開

はじめに

業務で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

サーバー側のコードは下記のコードです。

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}")
    }

このsuspendfun greet()をcoroutine化しています。
HTTPリクエストは(CPUに比べ)時間がかかるので非同期処理にしています。
https://qiita.com/duke105/items/b5be074c79c6bed4d560

ClientServerで使われている2つの型(HelloRequest, HelloReply)はどこで定義されているのかと思ったのですが、protoファイルで定義された型を参照していますね。

Client
val request = helloRequest { this.name = name }
Server
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