gRPCに入門する
公式ドキュメントをベースにgRPCに入門します。
gRPC
https://grpc.io/docs/guides/ より引用
RPC
遠隔手続き呼出し(英: remote procedure call、リモートプロシージャコール、略してRPC)とは、プログラムから別のアドレス空間(通常、共有ネットワーク上の別のコンピュータ上)にあるサブルーチンや手続きを実行することを可能にする技術
プログラマはローカルなサブルーチン呼び出しと基本的に同じコードをリモート呼び出しについても行う
https://www.geeksforgeeks.org/operating-system-remote-procedure-call-rpc/より引用
メリット
アプリケーションを実装するプログラマは、クライアントプログラムとサーバープロシージャだけを実装すれば良くなります。ネットワークプログラミングの詳細は、クライアントスタブ、RPCパッケージ、サーバースタブによって透過的に処理されます。
引数はクライアントスタブ・サーバースタブによって、マーシャリング・アンマーシャリング*1されるため、様々なプラットフォームにおけるコーディングを単純化することができます。
いろいろなRPCフレームワーク
gRPCの特徴
- Protocol Buffersを用いてインターフェースを定義
- 多言語でサーバー/クライアントのインターフェースの実装が可能
- HTTP/2による通信
Protocol Buffersの概要
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 配下に以下のファイル
- GreeterGrpc.java
が生成されたことが分かります。
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にアップしておきました。