永井 忠一 2025.12.7
Protocol Buffers のインストール
| Linux apt |
|---|
|
| Homebrew |
|
Goをインストール
| Linux apt |
|---|
|
| Homebrew |
|
(Kotlinは、適当なバージョンのOpenJDKと、IDE「IntelliJ IDEA Community Edition」を利用)
必要なプラグインと、パスの設定
| Go | |
|---|---|
| |
| .bashrc | .zshrc |
| |
利用する、例
| .proto |
|---|
syntax = "proto3";
package org.example.hello_rpc.v1;
option go_package = "test_buf/hello_rpc;hello_rpc";
option java_multiple_files = true;
message User {
int32 id = 1;
string name = 2;
}
|
テスト用の、Goのモジュールの作成
| Unix |
|---|
|
proto.Marshal() と .Unmarshal() の確認
| Go | Kotlin |
|---|---|
package main
import (
"fmt"
"os"
"google.golang.org/protobuf/proto"
"test_buf/hello_rpc"
)
func main() {
p := &(hello_rpc.User{
Id: 1,
Name: "test",
})
fmt.Println(p)
buf, err := proto.Marshal(p)
if err != nil {
os.Exit(1)
}
var q hello_rpc.User
if err := proto.Unmarshal(buf, &q); err != nil {
os.Exit(2)
}
fmt.Println(&q)
}
|
package org.example
import kotlin.system.exitProcess
import com.google.protobuf.InvalidProtocolBufferException
import org.example.hello_rpc.v1.User
fun main() {
val p = User.newBuilder()
.setId(1)
.setName("test")
.build()
println(p)
val buf: ByteArray = p.toByteArray()
val q = try {
User.parseFrom(buf)
} catch (e: InvalidProtocolBufferException) {
exitProcess(1)
}
println(q)
}
|
Kotlinでは、toByteArray() と parseFrom() メソッドを使う
Gradleビルドスクリプトの設定(Kotlin DSLに追加)
| build.gradle.kts |
|---|
plugins {
// ...snip...
id("com.google.protobuf") version "0.9.5"
}
// ...snip...
dependencies {
// ...snip...
// implementation("com.google.protobuf:protobuf-java:4.33.2")
implementation("com.google.protobuf:protobuf-kotlin:4.33.2")
}
// ...snip...
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.33.2"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
kotlin { }
}
}
}
}
|
(Gradleプラグイン、Maven Centralリポジトリを利用)
プロジェクト内の、.protoファイルの置き場所
$ tree src/main/ -I resources src/main/ ├── kotlin │ └── Main.kt └── proto └── user.proto 3 directories, 2 files
(ファイルを置いておくだけで、ビルドツールが探す)
さらに、必要なプラグイン
| Go | |
|---|---|
|
テスト用のサービス。四則演算
| .proto |
|---|
syntax = "proto3";
package org.example.hello_rpc.v1;
option go_package = "test_rpc/hello_rpc;hello_rpc";
option java_multiple_files = true;
message AdditionRequest {
int64 augend = 1; // left
int64 addend = 2; // right
}
message AdditionResponse {
int64 sum = 1; // answer
}
message SubtractionRequest {
int64 minuend = 1; // left
int64 subtrahend = 2; // right
}
message SubtractionResponse {
int64 difference = 1; // answer
}
message MultiplicationRequest {
int64 multiplicand = 1; // left
int64 multiplier = 2; // right
}
message MultiplicationResponse {
int64 product = 1; // answer
}
message DivisionRequest {
int64 dividend = 1; // left
int64 divisor = 2; // right
}
message DivisionResponse {
int64 quotient = 1; // answer
int64 remainder = 2;
}
service Calculator {
rpc Addition(AdditionRequest) returns (AdditionResponse);
rpc Subtraction(SubtractionRequest) returns (SubtractionResponse);
rpc Multiplication(MultiplicationRequest) returns (MultiplicationResponse);
rpc Division(DivisionRequest) returns (DivisionResponse);
}
|
準備
| Unix |
|---|
|
Goによる実装、実行例
| Go | |
|---|---|
| server | client |
package main
import (
"context"
"fmt"
"net"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
api "test_rpc/hello_rpc"
)
type server struct {
api.UnimplementedCalculatorServer
}
func (self *server) Addition(ctx context.Context, req *api.AdditionRequest) (*api.AdditionResponse, error) {
return &(api.AdditionResponse{
Sum: req.Augend + req.Addend,
}), nil
}
func (self *server) Subtraction(ctx context.Context, req *api.SubtractionRequest) (*api.SubtractionResponse, error) {
return &(api.SubtractionResponse{
Difference: req.Minuend - req.Subtrahend,
}), nil
}
func (self *server) Multiplication(ctx context.Context, req *api.MultiplicationRequest) (*api.MultiplicationResponse, error) {
return &(api.MultiplicationResponse{
Product: (req.Multiplicand)*(req.Multiplier),
}), nil
}
func (self *server) Division(ctx context.Context, req *api.DivisionRequest) (*api.DivisionResponse, error) {
if req.Divisor == 0 {
return nil, status.Error(codes.InvalidArgument, "division by zero")
}
return &(api.DivisionResponse{
Quotient: (req.Dividend)/(req.Divisor),
Remainder: (req.Dividend)%(req.Divisor),
}), nil
}
const PORT = 50051
func main() {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", PORT))
if err != nil {
os.Exit(1)
}
srv := grpc.NewServer()
api.RegisterCalculatorServer(srv, &(server{}))
if err := srv.Serve(lis); err != nil {
os.Exit(2)
}
}
|
package main
import (
"context"
"fmt"
"net"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
api "test_rpc/hello_rpc"
)
const (
HOST = "localhost"
PORT = 50051
)
func main() {
conn, err := grpc.NewClient(net.JoinHostPort(HOST, fmt.Sprint(PORT)), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
os.Exit(1)
}
defer conn.Close()
client := api.NewCalculatorClient(conn)
response, err := client.Division(context.Background(), &(api.DivisionRequest{
Dividend: 7,
Divisor: 3,
}))
if err != nil {
os.Exit(2)
}
fmt.Printf("quotient: %d\n", response.Quotient)
fmt.Printf("remainder: %d\n", response.Remainder)
}
|
Kotlinで実装
| Kotlin | |
|---|---|
| server |
package org.example
import io.grpc.*
import org.example.hello_rpc.v1.*
class CalculatorService : CalculatorGrpcKt.CalculatorCoroutineImplBase() {
override suspend fun addition(request: AdditionRequest): AdditionResponse {
return additionResponse {
sum = request.augend + request.addend
}
}
override suspend fun subtraction(request: SubtractionRequest): SubtractionResponse {
return subtractionResponse {
difference = request.minuend - request.subtrahend
}
}
override suspend fun multiplication(request: MultiplicationRequest): MultiplicationResponse {
return multiplicationResponse {
product = (request.multiplicand)*(request.multiplier)
}
}
override suspend fun division(request: DivisionRequest): DivisionResponse {
return try {
divisionResponse {
quotient = (request.dividend)/(request.divisor)
remainder = (request.dividend)%(request.divisor)
}
} catch (e: ArithmeticException) {
throw io.grpc.StatusException(io.grpc.Status.INVALID_ARGUMENT.withDescription("division by zero"))
}
}
}
private const val PORT = 50051
fun main() {
val server = ServerBuilder
.forPort(PORT)
.addService(CalculatorService())
.build()
server.start()
server.awaitTermination()
}
|
| client |
package org.example
import io.grpc.*
import kotlinx.coroutines.runBlocking
import org.example.hello_rpc.v1.*
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
private const val HOST = "localhost"
private const val PORT = 50051
fun main() = runBlocking {
val channel = ManagedChannelBuilder.forAddress(HOST, PORT)
.usePlaintext()
.build()
try {
val stub = CalculatorGrpcKt.CalculatorCoroutineStub(channel)
val request = divisionRequest {
dividend = 7
divisor = 3
}
val response = try {
stub.division(request)
} catch (e: Exception) {
exitProcess(1)
}
println("quotient: ${response.quotient}")
println("remainder: ${response.remainder}")
} finally {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS)
}
}
|
Gradleのビルドスクリプトに追加。(マルチモジュールのプロジェクト構成にしていない。プロジェクトのディレクトリ構成は、同じ)
| build.gradle.kts |
|---|
plugins {
// ...snip...
id("com.google.protobuf") version "0.9.6"
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
// ...snip...
dependencies {
// ...snip...
implementation("com.google.protobuf:protobuf-kotlin:4.33.2")
implementation("io.grpc:grpc-protobuf:1.77.0")
implementation("io.grpc:grpc-stub:1.77.0")
implementation("io.grpc:grpc-kotlin-stub:1.5.0")
implementation("io.grpc:grpc-netty:1.77.0")
}
// ...snip...
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.33.2"
}
plugins {
create("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.77.0"
}
create("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.5.0:jdk8@jar"
}
}
generateProtoTasks {
all().forEach { task ->
task.plugins {
create("grpc")
create("grpckt")
}
task.builtins {
create("kotlin")
}
}
}
}
application {
mainClass.set("org.example.MainKt")
}
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
mergeServiceFiles()
archiveClassifier.set("all")
}
|
クライアント側には、以下も必要
| build.gradle.kts |
|---|
// ...snip...
dependencies {
// ...snip...
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
// ...snip...
|
実行する、Gradleのタスク
言語境界をまたぐ例
| server | |||
|---|---|---|---|
| Go | Kotlin | ||
| client | Go |
|
|
| Kotlin |
|
|
|
(Nettyの警告が出ているが、正しく動作している)
© 2025 Tadakazu Nagai