RPC (Remote Procedure Call)

永井 忠一 2025.12.7


ドキュメント

IDL (Interface Description Language)

Protocol Buffers のインストール

Linux apt
$ sudo apt install protobuf-compiler elpa-protobuf-mode
$ protoc --version
libprotoc 3.21.12
Homebrew
% brew install protobuf
% protoc --version
libprotoc 33.1

Goをインストール

Linux apt
$ sudo apt install golang-go elpa-go-mode
$ go version
go version go1.22.2 linux/amd64
Homebrew
% brew install go
% go version
go version go1.25.5 darwin/arm64

(Kotlinは、適当なバージョンのOpenJDKと、IDE「IntelliJ IDEA Community Edition」を利用)

必要なプラグインと、パスの設定

Go
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
.bashrc.zshrc
export PATH="$PATH:$(go env GOPATH)/bin"

利用する、例

.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;
}

$ protoc user.proto --go_out=./ --go_opt=paths=source_relative --proto_path=./
$ ls user.pb.go
user.pb.go

テスト用の、Goのモジュールの作成

Unix
$ mkdir test_buf
$ cd test_buf
$ go mod init test_buf
go: creating new go.mod: module test_buf
$ mkdir hello_rpc
$ mv ../user.pb.go hello_rpc/
$ go get google.golang.org/protobuf
go: google.golang.org/protobuf@v1.36.10 requires go >= 1.23; switching to go1.24.11
go: upgraded go 1.22.2 => 1.23
go: added toolchain go1.24.11
go: added google.golang.org/protobuf v1.36.10
$ touch main.go

proto.Marshal() と .Unmarshal() の確認

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

$ go run main.go
id:1 name:"test"
id:1 name:"test"
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)
}

> Task :org.example.MainKt.main()
id: 1
name: "test"

id: 1
name: "test"

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

(ファイルを置いておくだけで、ビルドツールが探す)

LRPC

さらに、必要なプラグイン

Go
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

テスト用のサービス。四則演算

.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
$ mkdir test_rpc
$ cd test_rpc
test_rpc$ go mod init test_rpc
go: creating new go.mod: module test_rpc
test_rpc$ cd ..
$ protoc calc.proto --go_out=./ --go-grpc_out=./ --proto_path=./
$ cd test_rpc
test_rpc$ go get google.golang.org/protobuf
go: google.golang.org/protobuf@v1.36.11 requires go >= 1.23; switching to go1.24.11
go: upgraded go 1.22.2 => 1.23
go: added toolchain go1.24.11
go: added google.golang.org/protobuf v1.36.11
test_rpc$ go get google.golang.org/grpc
go: upgraded go 1.23 => 1.24.0
go: added golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82
go: added golang.org/x/sys v0.37.0
go: added golang.org/x/text v0.30.0
go: added google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8
go: added google.golang.org/grpc v1.77.0
test_rpc$ mkdir server client
test_rpc$ touch server/main.go client/main.go

test_rpc$ tree
.
├── client
│   └── main.go
├── go.mod
├── go.sum
├── hello_rpc
│   ├── calc.pb.go
│   └── calc_grpc.pb.go
└── server
    └── main.go

4 directories, 6 files

Goによる実装、実行例

Go
serverclient
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)
	}
}

test_rpc$ go run server/main.go
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)
}

test_rpc$ go run client/main.go
quotient: 2
remainder: 1

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のタスク

  1. generateProto(IDEの、GradleのUIでは「other」の所にある)
  2. shadowJar(IDEの、GradleのUIでは「shadow」の所にある)

言語境界をまたぐ例

server
GoKotlin
clientGo
test_rpc$ go run server/main.go

test_rpc$ go run client/main.go
quotient: 2
remainder: 1
test_rpc_server$ java --enable-native-access=ALL-UNNAMED -jar build/libs/test_rpc_server-1.0-SNAPSHOT-all.jar
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/home/nagai/...snip.../test_rpc_server/build/libs/test_rpc_server-1.0-SNAPSHOT-all.jar)
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release

test_rpc$ go run client/main.go
quotient: 2
remainder: 1
Kotlin
test_rpc$ go run server/main.go

test_rpc_client$ java --enable-native-access=ALL-UNNAMED -jar build/libs/test_rpc_client-1.0-SNAPSHOT-all.jar
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/home/nagai/...snip.../test_rpc_client/build/libs/test_rpc_client-1.0-SNAPSHOT-all.jar)
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
quotient: 2
remainder: 1
test_rpc_server$ java --enable-native-access=ALL-UNNAMED -jar build/libs/test_rpc_server-1.0-SNAPSHOT-all.jar
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/home/nagai/...snip.../test_rpc_server/build/libs/test_rpc_server-1.0-SNAPSHOT-all.jar)
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release

test_rpc_client$ java --enable-native-access=ALL-UNNAMED -jar build/libs/test_rpc_client-1.0-SNAPSHOT-all.jar
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/home/nagai/...snip.../test_rpc_client/build/libs/test_rpc_client-1.0-SNAPSHOT-all.jar)
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
quotient: 2
remainder: 1

(Nettyの警告が出ているが、正しく動作している)


© 2025 Tadakazu Nagai