技術メモ

技術メモ

ラフなメモ

gRPCに入門する

公式ドキュメントをベースにgRPCに入門します。

gRPC

f:id:tutuz:20190526085226p:plain

https://grpc.io/docs/guides/ より引用

RPC

遠隔手続き呼出し(英: remote procedure call、リモートプロシージャコール、略してRPC)とは、プログラムから別のアドレス空間(通常、共有ネットワーク上の別のコンピュータ上)にあるサブルーチンや手続きを実行することを可能にする技術

プログラマはローカルなサブルーチン呼び出しと基本的に同じコードをリモート呼び出しについても行う

f:id:tutuz:20190526075340p:plain

https://www.geeksforgeeks.org/operating-system-remote-procedure-call-rpc/より引用

メリット

  • アプリケーションを実装するプログラマは、クライアントプログラムとサーバープロシージャだけを実装すれば良くなります。ネットワークプログラミングの詳細は、クライアントスタブ、RPCパッケージ、サーバースタブによって透過的に処理されます。

  • 引数はクライアントスタブ・サーバースタブによって、マーシャリング・アンマーシャリング*1されるため、様々なプラットフォームにおけるコーディングを単純化することができます。

いろいろなRPCフレームワーク

gRPCの特徴

  • Protocol Buffersを用いてインターフェースを定義
  • 多言語でサーバー/クライアントのインターフェースの実装が可能
  • HTTP/2による通信

Guides – gRPC

Protocol Buffersの概要

  • Google製のInterface Definition Language*2
  • 言語やプラットフォームに依存しない、構造化データをシリアライズ機構
  • .proto ファイルでメッセージタイプを定義

https://developers.google.com/protocol-buffers/docs/proto3

gRPC Java入門

環境

IDL定義

Protocol Buffersで定義します。

// (1)
syntax = "proto3";

// (2)
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";

// (3)
// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello(HelloRequest) returns (HelloReply) {}
}

// (4)
// The request message containing the user's name.
message HelloRequest { string name = 1; }

// (5)
// The response message containing the greetings
message HelloReply { string message = 1; }

(1) Protocol Buffers の文法に proto3 を用いることを定義

(2) 各種オプション

No オプション名 サンプル設定値 説明
1 java_multiple_files true 自動生成されるクラスが1つのファイルにまとめて記述されるか、別ファイルとして生成されるか指定します。可読性の観点から別ファイルにします。
2 java_package io.grpc.examples.helloworld 自動生成されるファイルのパッケージ名を指定します。
3 java_outer_classname - (別ファイルとするため) 生成したい最外の Java クラス(従ってそのファイル名)のクラス名を指定します。
4 objc_class_prefix - (Javaを用いるため) .proto から生成される全ての Objective-C クラスと enum に対して接頭される Objective-C クラスプリフィクスを設定します。

(3) サービス定義

service でサービスのインターフェースを定義します。HelloRequestを引数にとり、HelloReplyをクライアントに返すサービスのインターフェース(SayHello関数)を定義します。

(4) (5) message定義

message で引数のHelloRequest, HelloReplyを定義します。messageは柔軟に定義することができます。詳細は公式ドキュメントを参照。

ビルド設定

build.gradle

plugins {
    id 'application'
    id 'com.google.protobuf' version '0.8.5'
}

repositories {
    mavenCentral()
    mavenLocal()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

def grpcVersion = '1.20.0'
def protobufVersion = '3.7.1'
def protocVersion = protobufVersion

dependencies {
    implementation "io.grpc:grpc-protobuf:${grpcVersion}"
    implementation "io.grpc:grpc-stub:${grpcVersion}"
    compileOnly "javax.annotation:javax.annotation-api:1.2"
    runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
    }
    generateProtoTasks {
        all()*.plugins { grpc {} }
    }
}

sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
    }
}

startScripts.enabled = false

task runServer(type: JavaExec) {
    classpath = sourceSets.main.runtimeClasspath
    main = 'io.github.tsuji.grpc.HelloWorldServer'
}

task runClient(type: JavaExec) {
    classpath = sourceSets.main.runtimeClasspath
    main = 'io.github.tsuji.grpc.HelloWorldClient'
}

自動生成

以下のコマンドでファイルを自動生成します。(gradlew build でもOK)

gradlew generateProto

build\generated\source\proto\main\java\io\grpc\examples\helloworld 配下に以下のファイル

  • HelloReplyOrBuilder.java
  • HelloRequest.java
  • HelloRequestOrBuilder.java
  • Helloworld.java
  • HelloReply.java

build\generated\source\proto\main\grpc\io\grpc\examples\helloworld 配下に以下のファイル

が生成されたことが分かります。

Serverの実装

tutorialsの例のコードを一部修正して用いることにします。

HelloWorldServer.java

package io.github.tsuji.grpc;

import java.io.IOException;
import java.util.logging.Logger;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.examples.helloworld.GreeterGrpc;
import io.grpc.examples.helloworld.HelloReply;
import io.grpc.examples.helloworld.HelloRequest;
import io.grpc.stub.StreamObserver;

/**
 * Server that manages startup/shutdown of a {@code Greeter} server.
 */
public class HelloWorldServer {
    private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());

    private Server server;

    private void start() throws IOException {
        /* The port on which the server should run */
        int port = 8888;
        server = ServerBuilder.forPort(port)
                .addService(new GreeterImpl())
                .build()
                .start();
        logger.info("Server started, listening on " + port);
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                // Use stderr here since the logger may have been reset by its JVM shutdown hook.
                System.err.println("*** shutting down gRPC server since JVM is shutting down");
                HelloWorldServer.this.stop();
                System.err.println("*** server shut down");
            }
        });
    }

    private void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    /**
    * Await termination on the main thread since the grpc library uses daemon threads.
    */
    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    /**
    * Main launches the server from the command line.
    */
    public static void main(String[] args) throws IOException, InterruptedException {
        final HelloWorldServer server = new HelloWorldServer();
        server.start();
        server.blockUntilShutdown();
    }

    static class GreeterImpl extends GreeterGrpc.GreeterImplBase {

        @Override
        public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
            logger.info("Request received.");
            HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName().toUpperCase()).build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        }
    }
}

サーバー側の実装は、ServerBuilderのaddServiceを用いて、公開するGrpcServiceを登録します。

   server = ServerBuilder.forPort(port)
            .addService(new GreeterImpl())
            .build()
            .start();

サービスは自動生成されたベースクラスを継承したクラスを作成して実装します。UnaryではStreamObserver#onNextを1度だけ呼び出すことでき、レスポンスを記述します。処理の完了はStreamObserver#onCompletedで示します。

   static class GreeterImpl extends GreeterGrpc.GreeterImplBase {

        @Override
        public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
            logger.info("Request received.");
            HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName().toUpperCase()).build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        }
    }

なお、自動生成された継承元のクラスは以下のような抽象クラスになっています。

GreeterImplBase.java

  /**
   * <pre>
   * The greeting service definition.
   * </pre>
   */
  public static abstract class GreeterImplBase implements io.grpc.BindableService {

    /**
     * <pre>
     * Sends a greeting
     * </pre>
     */
    public void sayHello(io.grpc.examples.helloworld.HelloRequest request,
        io.grpc.stub.StreamObserver<io.grpc.examples.helloworld.HelloReply> responseObserver) {
      asyncUnimplementedUnaryCall(getSayHelloMethod(), responseObserver);
    }

    @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
      return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
          .addMethod(
            getSayHelloMethod(),
            asyncUnaryCall(
              new MethodHandlers<
                io.grpc.examples.helloworld.HelloRequest,
                io.grpc.examples.helloworld.HelloReply>(
                  this, METHODID_SAY_HELLO)))
          .build();
    }
  }

Clientの実装

HelloWorldClient.java

package io.github.tsuji.grpc;

import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import io.grpc.examples.helloworld.GreeterGrpc;
import io.grpc.examples.helloworld.HelloReply;
import io.grpc.examples.helloworld.HelloRequest;

/**
 * A simple client that requests a greeting from the {@link HelloWorldServer}.
 */
public class HelloWorldClient {
    private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName());

    private final ManagedChannel channel;
    private final GreeterGrpc.GreeterBlockingStub blockingStub;

    /** Construct client connecting to HelloWorld server at {@code host:port}. */
    public HelloWorldClient(String host, int port) {
        this(ManagedChannelBuilder.forAddress(host, port)
                // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
                // needing certificates.
                .usePlaintext()
                .build());
    }

    /** Construct client for accessing HelloWorld server using the existing channel. */
    HelloWorldClient(ManagedChannel channel) {
        this.channel = channel;
        blockingStub = GreeterGrpc.newBlockingStub(channel);
    }

    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    /** Say hello to server. */
    public void greet(String name) {
        logger.info("Will try to greet " + name + " ...");
        HelloRequest request = HelloRequest.newBuilder().setName(name).build();
        HelloReply response;
        try {
            response = blockingStub.sayHello(request);
        } catch (StatusRuntimeException e) {
            logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
            return;
        }
        logger.info("Greeting: " + response.getMessage());
    }

    /**
    * Greet server. If provided, the first element of {@code args} is the name to use in the
    * greeting.
    */
    public static void main(String[] args) throws Exception {
        HelloWorldClient client = new HelloWorldClient("localhost", 8888);
        try {
            /* Access a service running on the local machine on port 8888 */
            String user = "world";
            if (args.length > 0) {
                user = args[0]; /* Use the arg as the name to greet if provided */
            }
            client.greet(user);
        } finally {
            client.shutdown();
        }
    }
}

クライアントでは、チャネルとスタブを用います。

   private final ManagedChannel channel;
    private final GreeterGrpc.GreeterBlockingStub blockingStub;

チャネルを生成します。

   public HelloWorldClient(String host, int port) {
        this(ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext()
                .build());
    }

スタブを通じて、リクエストを生成します。

   public void greet(String name) {
        logger.info("Will try to greet " + name + " ...");
        HelloRequest request = HelloRequest.newBuilder().setName(name).build();
        HelloReply response;
        try {
            response = blockingStub.sayHello(request);
        } catch (StatusRuntimeException e) {
            logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
            return;
        }
        logger.info("Greeting: " + response.getMessage());
    }

動作確認

Serverの起動

gradlew runServer

> Task :runServer
情報: Server started, listening on 8888 [日 5月 26 11:24:38 JST 2019]
情報: Request received. [日 5月 26 11:24:47 JST 2019]
<===========--> 85% EXECUTING [15s]
> :runServer

Clientの起動(Serverとは別ターミナル)

gradlew runClient

> Task :runClient
情報: Will try to greet world ... [日 5月 26 11:24:46 JST 2019]
情報: Greeting: Hello WORLD [日 5月 26 11:24:48 JST 2019]

疎通できていることが確認できました。

まとめ

gRPCのRPCのうち、最もシンプルなUnary RPCを試してみました。

ソースはGitHubにアップしておきました。

github.com

参考

*1:異なる2つのシステム間で、データを交換できるようにデータを操作する処理

*2:ソフトウェアコンポーネント間のインタフェースを記述するのに使われるコンピュータ言語。他にはSOAPやThrift IDLなど