KotlinでgRPCサーバーを作ろう

はじめに

初めまして、株式会社ビデオマーケットでサーバーサイドエンジニアを担当しているrhyskです。

弊社では以前よりサーバーサイド言語にKotlinを採用していましたが、RestAPIで提供しているサービスしかなくgRPCを利用したサービスはまだありませんでした。

今年から新たなプロジェクトが開始した折にメンバーからgRPCを使ってみたいとの声が多数上がったため、今回採用することになりました。

今更ではありますがKotlinでgRPCを動かす際のチュートリアルを作成したのでぜひ参考にしてみてください。

フレームワークはSpring Bootを使用します。

gRPCとは

gRPCとはGoogleが開発したオープンソースのRPC(Remote Procedure Call)システムです。 サーバとクライアント間はHTTP/2で通信を行います。

Protocol Buffers(プロトコルバッファー)と呼ばれるインタフェース記述言語(IDL)を書くだけで、サーバーとクライアントでやりとりするためのソースコードを自動で生成できます。

つまり、Protocol Buffersは、「通信するための仕様を決めることができ、その仕様通りに実装することができる」ということです。

メンテされない仕様書を作る必要なく、インタフェース仕様の会話ができるのは素晴らしいですね。

準備するもの

  • MacBook Pro
  • IDE
  • gRPC UI(gRPCのクライアントツール)

環境構築

Spring Bootの雛形作成

Kotlinで使用できるフレームワークも結構増えてきましたがサーバーサイドで使える機能も多いのでここではSpring Bootを使用することにします。 Spring Bootの雛形を作るための専用のサイトSpring Initializrがあるのでここから必要な項目を入力してダウンロードしてください。

https://start.spring.io/

f:id:rhysk:20210930163520p:plain

gRPCライブラリのインストール

以下のライブラリをダウンロードしてきた雛形にある build.gradle.kts に適用していきます。

github.com

github.com

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.google.protobuf.gradle.*

// これを追加↓↓↓
val protobufVersion = "3.17.0"
val grpcVersion = "1.16.1"
val grpcKotlinVersion = "0.1.2"
val grpcSpringBootVersion = "4.5.6"
// これを追加↑↑↑

plugins {
    id("org.springframework.boot") version "2.5.5"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.5.31"
    kotlin("plugin.spring") version "1.5.31"
    // これを追加↓↓↓
    id("idea")
    id("com.google.protobuf") version "0.8.16"
    // これを追加↑↑↑
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    // これを追加↓↓↓
    implementation("io.github.lognet:grpc-spring-boot-starter:$grpcSpringBootVersion")
    implementation("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion")
    // これを追加↑↑↑
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

// これを追加↓↓↓
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:$protobufVersion"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion"
        }
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion"
        }
    }
    generateProtoTasks {
        ofSourceSet("main").forEach {
            it.plugins {
                id("grpc")
                id("grpckt")
            }
        }
    }
}
// これを追加↑↑↑

Protocol Buffersを作成

今回はユーザーIDを渡すとメールアドレスが返ってくるようなものをIFとして定義してみます。 src/main/proto ディレクトリを作成してそこに以下のような user_provider.proto を作ってみることにします。

user_provider.proto

syntax = "proto3";

option java_package = "com.example.kotlingrpc.proto";

// ユーザーサービス
service UserService {
  // ユーザー取得
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
// ユーザー取得リクエスト
message GetUserRequest {
  int32 id = 1; // ユーザーID
}
// ユーザー取得レスポンス
message GetUserResponse {
  string mail = 1; // メール
}

ソースコードの生成

作ったprotoファイルに基づいて通信するためのサーバーのソースコードを自動生成します。 ターミナルなどでプロジェクトのルートディレクトリに移動して以下のコマンドを実行します。 BUILD SUCCESSFULと表示されたらソースコードが build/generated 配下に自動生成されています。

./gradlew generateProto

f:id:rhysk:20210930181805p:plain

実装

自動生成されたソースコードを継承してメソッドをオーバーライドします。

ここではサンプルとして実装しているのでユーザーID1、2の時に何かしらメールアドレスが返ってくるような処理にしています。

UserController.kt

package com.example.kotlingrpc.controller

import com.example.kotlingrpc.proto.UserProvider
import com.example.kotlingrpc.proto.UserServiceGrpcKt
import org.lognet.springboot.grpc.GRpcService

@GRpcService
class UserController : UserServiceGrpcKt.UserServiceCoroutineImplBase() {

    /**
     * ユーザー取得
     */
    override suspend fun getUser(request: UserProvider.GetUserRequest): UserProvider.GetUserResponse {
        return UserProvider.GetUserResponse.newBuilder().setMail(fetchMail(request.id)).build()
    }

    /**
     * メールアドレスを返す
     */
    private fun fetchMail(id: Int) = when (id) {
        1 -> "video@videomarket.co.jp"
        2 -> "market@videomarket.co.jp"
        else -> ""
    }
}

サーバー起動

ターミナルなどでプロジェクトのルートディレクトリに移動して以下のコマンドを実行します。

./gradlew bootRun

サーバーが起動すると以下のようなログが流れます。

2021-09-30 18:35:06.777  INFO 52485 --- [  restartedMain] o.l.springboot.grpc.GRpcServerRunner     : gRPC Server started, listening on port 6565.
2021-09-30 18:35:06.784  INFO 52485 --- [  restartedMain] c.e.kotlingrpc.KotlinGrpcApplicationKt   : Started KotlinGrpcApplicationKt in 1.575 seconds (JVM running for 1.938)

動作確認

gRPCサーバーが立ち上がったら実行してみましょう。

gRPC UIなどのクライアントツールを使うと動作確認が簡単です。

github.com

ポート6565で立ち上がったサーバー情報にアクセスするためには以下のコマンドを実行します。

grpcui --plaintext localhost:6565

上記のコマンドを実行するとリフレクションAPIがサポートされていない旨のメッセージが出ると思います。

Failed to compute set of methods to expose: server does not support the reflection API

その場合は以下のようなクラスを用意してリフレクションサポートをしてあげてください。 こうすることでgRPCで提供しているサービスとメソッドを知ることができるようになります。

GRpcBuildConfig.kt

package com.example.kotlingrpc

import io.grpc.ServerBuilder
import io.grpc.protobuf.services.ProtoReflectionService
import org.lognet.springboot.grpc.GRpcServerBuilderConfigurer
import org.springframework.stereotype.Component

@Component
class GRpcBuildConfig: GRpcServerBuilderConfigurer() {
    override fun configure(serverBuilder: ServerBuilder<*>) {
        serverBuilder.addService(ProtoReflectionService.newInstance())
    }
}

上記のメッセージが解消されたらもう一度サーバーを再起動してgrpcuiコマンドも再実行します。

gRPC UIが立ち上がるとidを入力して実行してみます。

f:id:rhysk:20210930184828p:plain

無事メールアドレスが返却されました!

f:id:rhysk:20210930184842p:plain

終わりに

いかがでしたか?

KotlinもGradleもSpringもgRPCもその他のライブラリも入れ替わりがかなり早いですが、どんどんシンプルに実装できるように進化しているのでしっかりついていきたいですね!

弊社のサービスもKotlin × gRPC × Spring Bootで稼働し始めたのでまた別の機会に細かい情報を発信できればと思います。