gRPCを試す
gRPCを試したメモです。
JavaとGoでgRPCを試してみたメモです。
- gRPC
- Protocol Buffers
- Protocol Buffers インストール
- IDL定義
- Java
- Go
- Java Server - Go Client
- Java Client - Go Server
以前Apache ThriftでRPCを試したので、 gRPCも試してみようと思います。
JavaとGoで実装してみます。
Macで下記のバージョンを使用してます。
Protocol Buffers
Protocol Buffersでデータをシリアライズ/デシリアライズします。
Apache Thriftと同様、IDL(インターフェース定義言語)でインターフェースを定義します。
そして各言語ごとのソースを生成して実装していきます。
developers.google.com
Protocol Buffers インストール
Protocol Buffersをインストールします。
Protocol Buffersにはproto2とproto3のversionがありますが、proto3系をインストールします。
下記から自分の環境用のファイルをダウンロードします。
Macではprotoc-3.5.1-osx-x86_64.zipをダウンロードしました。
https://github.com/google/protobuf/releases
zipを展開して、protocを/usr/local/bin 配下に配置します。
$ mkdir protoc $ cd protoc $ curl -L -O https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-osx-x86_64.zip $ unzip protoc-3.5.1-osx-x86_64.zip $ mv bin/protoc /usr/local/bin $ mv include/google /usr/local/include/ $ chmod 755 /usr/local/bin/protoc
確認
$ protoc --version
libprotoc 3.5.1
IDL定義
IDLファイルを作成し、インターフェースを定義していきます。
Protocol Buffersはproto2とproto3で記述方法が異なります。今回はproto3で記述します。
proto3の記述方法や、各言語でどのようなソースが生成されるかなどは下記に記載してあります。
https://developers.google.com/protocol-buffers/docs/proto3
https://developers.google.com/protocol-buffers/docs/style
https://qiita.com/CyLomw/items/9aa4551bd6bb9c0818b6 (素晴らしい日本語訳)
.protoという拡張子のファイルを作成し記述していきます。
myService.protoというファイルを作成して、下記のように定義しました。
(以前、Apache Thriftを試した時と同じインターフェースにしています)
myService.proto
syntax = "proto3"; // (1) package exampleGrpc; // go options option go_package = "exampleGrpc"; // (2) // java options option java_package = "com.example.grpc"; // (3) option java_multiple_files = true; // (4) // (5) service PeopleService { rpc SearchByName(SearchRequest) returns (SearchResponse) {} } // (6) message SearchRequest { string query = 1; } // (7) message SearchResponse { repeated Person people = 1; } // (8) message Person { enum Country { AMERICA = 0; JAPAN = 1; CANADA = 2; } string name = 1; int32 age = 2; Country country = 3; string hobby = 4; }
(1) versionがproto3であることを宣言します。
(2) goのpackage名を宣言します。
(3) javaのpackage名を宣言します。
(4)
java_multiple_files=trueを指定しない場合、生成されたソースが1つのファイルに記述されます。
(この例の場合、ソース生成後のMyService.javaにすべて記述される)
可読性も悪いのでtrueを指定したほうが良いと思います。
(5)
serviceでサービスのインターフェースを定義します。
SearchByName関数を定義します。SearchRequestが引数、SearchResponseが戻り値になります。
(6)
messageで引数のSearchRequestを定義します。
型と名前を定義し、イコールの後にタグ番号を振る必要があります。
タグ番号は1以上でmessage定義内でユニークである必要があります。
ソースを生成した際に型が各言語の何の型になるかは下記に記載されています。
https://developers.google.com/protocol-buffers/docs/proto3#scalar
(7)
同様に戻り値のSearchResponseを定義します。
ここの型では別messageで定義しているPersonを指定しています。
repeatedをつけるとこの要素が繰り返されることを意味しています。(Listのように)
(8)
Personを定義します。
message内でenumのCountryを定義し使用しています。
enumの定義の最初の要素は必ず0にマップされている必要があります。
ちなみにApache ThriftのIDLファイルと比較するとこんな感じです。
Thriftのほうがコンパクトに直感的に書ける気がします(個人的に)。
MyService.thrift
namespace java com.example.thrift namespace perl exampleThrift enum Country { AMERICA=0, JAPAN=1, CANADA=2 } struct Person { 1: required string name; 2: required i32 age; 3: required Country country; 4: string hobby; } service PeopleService { list<Person> searchByName(1:string query) }
Java
java用のソースを生成し、serverとclientを作成してみます。
gradleプロジェクトを作成します。
src/main/protoというディレクトリを作成し、myService.protoを配置します。
mkdir src/main/proto/ mv myService.proto src/main/proto/
build.gradleを記述します。
依存関係に、grpcのライブラリを追加し、
・grpc-netty
・grpc-protobuf
・grpc-stub
Protocol Buffersのコード生成用のライブラリを追加します。
・protobuf-gradle-plugin
build.gradle
group 'com.example.grpc' version '1.0-SNAPSHOT' apply plugin: 'java' apply plugin: 'com.google.protobuf' apply plugin: 'idea' sourceCompatibility = 1.8 targetCompatibility = 1.8 buildscript { repositories { mavenCentral() } dependencies { classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3' } } repositories { mavenCentral() mavenLocal() } def grpcVersion = '1.10.0' // CURRENT_GRPC_VERSION dependencies { compile "io.grpc:grpc-netty:${grpcVersion}" compile "io.grpc:grpc-protobuf:${grpcVersion}" compile "io.grpc:grpc-stub:${grpcVersion}" } protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.5.1-1' } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { all()*.plugins { grpc {} } } } // Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. sourceSets { main { java { srcDirs 'build/generated/source/proto/main/grpc' srcDirs 'build/generated/source/proto/main/java' } } }
ビルドしてソースを生成します。
$ ./gradlew build
BUILD SUCCESSFUL in 2s
ソースが生成されている事を確認してみます。
IDLで定義したserviceと各messageのmodelが生成されていることが分かります。
$ ll build/generated/source/proto/main/grpc/com/example/grpc total 24 drwxr-xr-x 3 pppurple staff 102B 3 6 21:49 . drwxr-xr-x 3 pppurple staff 102B 3 6 21:49 .. -rw-r--r-- 1 pppurple staff 10K 3 6 21:49 PeopleServiceGrpc.java
$ ll build/generated/source/proto/main/java/com/example/grpc total 176 drwxr-xr-x 9 pppurple staff 306B 3 6 21:49 . drwxr-xr-x 3 pppurple staff 102B 3 6 21:49 .. -rw-r--r-- 1 pppurple staff 3.9K 3 6 21:49 MyService.java -rw-r--r-- 1 pppurple staff 25K 3 6 21:49 Person.java -rw-r--r-- 1 pppurple staff 913B 3 6 21:49 PersonOrBuilder.java -rw-r--r-- 1 pppurple staff 16K 3 6 21:49 SearchRequest.java -rw-r--r-- 1 pppurple staff 469B 3 6 21:49 SearchRequestOrBuilder.java -rw-r--r-- 1 pppurple staff 24K 3 6 21:49 SearchResponse.java -rw-r--r-- 1 pppurple staff 948B 3 6 21:49 SearchResponseOrBuilder.java
Server
javaでserverを実装します。
IDLで定義したpackageと同じpackage(com.example.grpc)を作成し、
その下にMyServiceServer.javaを作成します。
MyServiceServer.java
package com.example.grpc; import com.example.grpc.Person.Country; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.util.Arrays; import java.util.List; public class MyServiceServer { private Server server; private void start() throws IOException { int port = 50051; server = ServerBuilder.forPort(port) .addService(new PeopleServiceImpl()) .build() .start(); System.out.println("Server started... (port=" + port + ")"); Runtime.getRuntime() .addShutdownHook(new Thread(() -> { System.err.println("*** shutting down gRPC server since JVM is shutting down"); MyServiceServer.this.stop(); System.err.println("*** server shut down"); })); } private void stop() { if (server != null) { server.shutdown(); } } private void blockUntilShutdown() throws InterruptedException { if (server != null) { server.awaitTermination(); } } public static void main(String[] args) throws IOException, InterruptedException { final MyServiceServer server = new MyServiceServer(); server.start(); server.blockUntilShutdown(); } // (1) static class PeopleServiceImpl extends PeopleServiceGrpc.PeopleServiceImplBase { @Override public void searchByName(SearchRequest request, StreamObserver<SearchResponse> responseObserver) { System.out.println("query=" + request.getQuery()); // DBから検索していると想定 // search from DB Person alice = Person.newBuilder().setName("Alice Wall") .setAge(20) .setCountry(Country.JAPAN) .setHobby("tennis") .build(); Person bobby = Person.newBuilder().setName("Bobby Wall") .setAge(33) .setCountry(Country.CANADA) .setHobby("music") .build(); List<Person> people = Arrays.asList(alice, bobby); // 検索結果 SearchResponse response = SearchResponse.newBuilder() .addAllPeople(people) .build(); responseObserver.onNext(response); responseObserver.onCompleted(); } } }
(1)
PeopleServiceGrpc.javaにIDLで定義した関数を持つabstract class(PeopleServiceImplBase)が生成されているので、実装します。
引数のSearchRequestのqueryを元にDBからPersonを検索して、結果のSearchResponseを返します。
DBは構築してないので、擬似的にDBから検索したものとして、PersonのListを返しています。
Personなど、IDLの各messageから生成されたmodelはBuilderパターンで生成することが出来ます。
Client
javaでclientを実装します。
com.example.grpcパッケージ配下にMyServiceClient.javaを作成します。
MyServiceClient.java
package com.example.grpc; import com.example.grpc.PeopleServiceGrpc.PeopleServiceBlockingStub; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; import java.util.concurrent.TimeUnit; public class MyServiceClient { private final ManagedChannel channel; private final PeopleServiceBlockingStub blockingStub; private MyServiceClient(String host, int port) { this(ManagedChannelBuilder.forAddress(host, port) .usePlaintext(true) .build() ); } private MyServiceClient(ManagedChannel channel) { this.channel = channel; blockingStub = PeopleServiceGrpc.newBlockingStub(channel); } private void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } // (1) private SearchResponse searchByName(String query) { SearchRequest request = SearchRequest.newBuilder() .setQuery(query) .build(); SearchResponse response = null; try { response = blockingStub.searchByName(request); } catch (StatusRuntimeException e) { System.err.println("RPC failed. " + e); } return response; } public static void main(String[] args) throws InterruptedException { MyServiceClient client = new MyServiceClient("localhost", 50051); SearchResponse response; try { response = client.searchByName("Wall"); } finally { client.shutdown(); } // (2) // 結果を出力 System.out.println("[response]"); response.getPeopleList() .forEach(System.out::println); } }
(1)
SearchRequestを生成し(ここではqueryは固定文字列)、
PeopleServiceStubのsearchByName()を呼び出します。
(2)
確認用に結果のSearchResponseを標準出力してます。
Go
go用のソースを生成し、serverとclientを作成してみます。
grpcとProtocol Buffersのpackageをインストールします。
$ go get -u google.golang.org/grpc $ go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
my_serviceディレクトリを作成し、その下にmyService.protoを配置します。
$ mkdir my_service $ ll total 8 drwxr-xr-x 3 pppurple staff 102B 3 8 21:38 . drwxr-xr-x 7 pppurple staff 238B 3 8 21:38 .. -rw-r--r-- 1 pppurple staff 579B 3 6 21:41 myService.proto
protocコマンドでgoのソースコードを生成します。
$ protoc -I my_service --go_out=plugins=grpc:my_service myService.proto
my_serviceディレクトリにmyService.pb.goが生成されました。
$ ll my_service total 24 drwxr-xr-x 4 pppurple staff 136B 3 8 21:56 . drwxr-xr-x 7 pppurple staff 238B 3 8 21:38 .. -rw-r--r-- 1 pppurple staff 7.8K 3 8 21:56 myService.pb.go -rw-r--r-- 1 pppurple staff 579B 3 6 21:41 myService.proto
Server
goでserverを実装します。
サーバ用のディレクトリ(my_service_server)を作成し、main.goを作成します。
my_service_server/main.go
package main import ( "fmt" "net" // (1) pb "github.com/pppurple/go_examples/grpc_example/my_service" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) const ( port = ":50051" ) type server struct{} // (2) func (s *server) SearchByName(ctx context.Context, request *pb.SearchRequest) (*pb.SearchResponse, error) { fmt.Println("query=" + request.Query) // DBから検索していると想定 alice := pb.Person{ Name: "Alice Wall", Age: 20, Country: pb.Person_JAPAN, Hobby: "tennis", } bobby := pb.Person{ Name: "Bobby Wall", Age: 33, Country: pb.Person_CANADA, Hobby: "music", } people := []*pb.Person{&alice, &bobby} return &pb.SearchResponse{People: people}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { fmt.Printf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterPeopleServiceServer(s, &server{}) fmt.Println("Server started... (port=" + port + ")") reflection.Register(s) if err := s.Serve(lis); err != nil { fmt.Printf("failed to serve: %v", err) } }
(1)
生成されたpackageをpbとしてimport
(2)
javaの場合と同様にSearchByName()を実装します。
引数のSearchRequestのqueryを元にDBからPersonを検索して、結果のSearchResponseを返します。
DBは構築してないので、擬似的にDBから検索したものとして、Personのsliceを返しています。
Client
goでclientを実装します。
クライアント用のディレクトリ(my_service_client)を作成し、main.goを作成します。
my_service_client/main.go
package main import ( "fmt" "time" pb "github.com/pppurple/go_examples/grpc_example/my_service" "golang.org/x/net/context" "google.golang.org/grpc" ) const ( host = "localhost" port = ":50051" query = "Wall" ) func main() { // Set up a connection to the server. conn, err := grpc.Dial(host+port, grpc.WithInsecure()) if err != nil { fmt.Printf("did not connect: %v", err) } defer conn.Close() client := pb.NewPeopleServiceClient(conn) // Contact the server and print out its response. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // (1) res, err := client.SearchByName(ctx, &pb.SearchRequest{Query: query}) if err != nil { fmt.Printf("could not greet: %v", err) } // (2) fmt.Println("[Response]") for _, v := range res.People { fmt.Println(v) } }
(1)
SearchRequestを生成し(ここではqueryは固定文字列)、
PeopleServiceClient.searchByName()を呼び出します。
(2)
確認用に結果のSearchResponseを標準出力してます。
Java Server - Go Client
Java serverとGo clientの組み合わせで動かしてみます。
java server起動
Server started... (port=50051)
go client実行。
きちんとレスポンスが表示されています。
$ cd my_service_client $ go run main.go [Response] name:"Alice Wall" age:20 country:JAPAN hobby:"tennis" name:"Bobby Wall" age:33 country:CANADA hobby:"music"
java server側ログ
clientからの検索クエリが表示されています。
query=Wall
Java Client - Go Server
Go serverとjava clientの組み合わせで動かしてみます。
go server起動
$ cd my_service_server $ go run main.go Server started... (port=:50051)
java client実行。
きちんとレスポンスが表示されています。
[response] name: "Alice Wall" age: 20 country: JAPAN hobby: "tennis" name: "Bobby Wall" age: 33 country: CANADA hobby: "music"
go server側ログ
clientからの検索クエリが表示されています。
query=Wall
こんなとこです。
ソースは下記にあげました。
github.com
github.com
参考
・Protocol Buffers
https://gist.github.com/sofyanhadia/37787e5ed098c97919b8c593f0ec44d8
https://qiita.com/CyLomw/items/9aa4551bd6bb9c0818b6
・java
https://github.com/grpc/grpc-java
https://developers.google.com/protocol-buffers/docs/reference/java-generated
・go
https://developers.google.com/protocol-buffers/docs/reference/go-generated