gRPCを試す

gRPCを試したメモです。


JavaとGoでgRPCを試してみたメモです。


以前Apache ThriftでRPCを試したので、 gRPCも試してみようと思います。

JavaとGoで実装してみます。

Macで下記のバージョンを使用してます。

  • protoc 3.5.1
  • grpc-java 1.10.0
  • grpc-go 1.10.0
  • java 1.8
  • go 1.9

gRPC

gRPCはGoogleが開発したオープンソースのRPCフレームワークです。
デフォルトでProtocol Buffersに対応しています。
grpc.io

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