gRPC/Go で簡単なサーバーとクライアントを実装する

はじめに

本記事ではgRPCを利用したサーバーとクライアントを実装します。マイクロサービス開発などでよく使われるgRPCですが、ちゃんと調べたことがなかったので簡単なサーバとクライアントを実装してまとめました。
今回の例として使用したリポジトリGitHubにあります。

github.com

gRPCとは

gRPCはGoogleによって開発されたリモートプロシージャコールシステム(RPC System)です。 gRPCを使用するとクライアントがサーバーのメソッドを直接呼び出すことができます。マイクロサービスでは多くのサービスが存在し、それぞれのサービスを利用する必要があります。そこでgRPCを使用すると他のマイクロサービスのメソッドを容易に呼び出すことが可能になるという利点があります。

f:id:y-zumi:20190907004223j:plain

RPC(遠隔手続き呼び出し)とは

RPCとはクライアントがサーバーのルーチンをネットワークを介して呼び出すことです。RPCではクライアントとサーバーのインターフェースを統一して実装します。クライアントとサーバー間で手続きのインターフェースを提供する方法としてインターフェース記述言語(IDL)が使われます。

Protocol Buffersとは

Protocol Buffersはインターフェース記述言語(IDL)の一つです。gRPCはProtocol Buffersをデフォルトで使用しています。Protocol Buffersは記述がシンプルで、パースも高速です。Protocol BuffersはRPCのプロトコルを記述するだけでなく、データ構造の定義としても利用することができます。プロトコルやデータ構造は.protoというファイルで定義し、protocプログラムによりコンパイルされることで、各クライアントに適合されたコード(xxx.pb.go, xxx.pg.swiftなど)を生成します。
一つのプロトコルファイルから、各クライアントに対応したファイルを生成できる容易さにより、クライアントのプラットフォームが異なる場合でもRPCを実現可能となっています。公式ドキュメントには、Protocol BuffersとXMLの性能差を中心にProtocol Buffersの特徴が記載されています。

gRPCを使用してサーバーとクライアントを実装する

今回は例として 「UserをIDで検索するサーバーを実装し、クライアントからgRPC通信でUserを取得する」 までを実装します。

手順は以下のようになります。

  • Protocol Buffersによるインタフェースとメッセージの定義
  • gRPC Server の実装
  • gRPC Client の実装

準備

protoファイルからgo用のスキーマを生成するためにProtocol Buffersのコンパイラであるprotocをインストールする必要があります。

protocはhomebrewを使用して以下のコマンドでインストールできます。

> brew install protobuf
> protoc --version
libprotoc 3.9.1

Protocol Buffersによるインタフェースとメッセージの定義

まずは、Protocol Buffersを使用して以下のインタフェースとメッセージ構造を定義します。

  • サーバーが提供する機能のインタフェースを定義する
    • FindByID
  • リクエストとレスポンスのメッセージの構造を定義する
    • FindByIDRequest
    • FindByIDResponse
    • User

サーバーが提供する機能のインタフェースを定義する

サーバーが実際に提供するgRPCエンドポイントのインタフェースは以下のようになります。FindByIDがRPCとして定義されており、後述するFindByIDRequestFindByIDResponseを引数と戻り値に定義しています。FindByIDは、サーバー側で実態が実装され、クライアント側からコールしてレスポンスを取得する時のインターフェースとなります。

service Users {
    rpc FindByID (FindByIDRequest) returns (FindByIDResponse) {}
}

リクエストとレスポンスのメッセージの構造を定義する

リクエストとレスポンスは以下のようになります。FindByIDRequestUserのIDのみを定義しています。FindByIDResponseUserを所持しています。

message FindByIDRequest {
    string id = 1;
}

message FindByIDResponse {
    User user = 1;
}

message User {
    string id = 1;
    string name = 2;
}

protoファイルをコンパイルする

.protoファイルを定義したのでprotocコンパイルします。これにより、クライアントとサーバーで実際に使用されるデータアクセス用のクラスが生成されます。

コンパイルには以下のコマンドを使用します。コンパイル後にはuser.pb.goファイルが生成されているはずです。このコマンドはy-zumi/grpc-goで使用した時のものであるため、各自の環境ではでパス等を変更してください。

> protoc -I proto/ proto/user.proto --go_out=plugins=grpc:$GOPATH/src/github.com/y-zumi/grpc-go/proto/
> ls proto/
user.pb.go user.proto

Serverの実装

次に先ほど生成したuser.pb.goをもとにサーバーを実装します。
gRPC ServerにはUserService構造体が定義されており、UserServiceはポインタレシーバであるFindByIDを持っています。 FindByIDは引数にFindByIDRequestをとり、戻り値としてFindByIDResponseを返しています。

// UserService presents pb.UsersService
type UserService struct{}

// FindByID find user by user id
func (s *UserService) FindByID(ctx context.Context, req *pb.FindByIDRequest) (*pb.FindByIDResponse, error) {
    return &pb.FindByIDResponse{
        User: &pb.User{
            Id:   req.Id,
            Name: "Sample",
        },
    }, nil
}

サーバーのmain関数ではgRPC Serverの起動を行います。

func main() {
    // Start listening port
    lis, err := net.Listen("tcp", ":5001")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // Register UsersServer to gRPC Server
    s := grpc.NewServer()
    pb.RegisterUsersServer(s, &UserService{})

    // Add grpc.reflection.v1alpha.ServerReflection
    reflection.Register(s)

    // Start server
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

以上でサーバー側の実装は完了しました。次はクライアントの実装します。

Clientの実装

クライアントの実装は以下のようになります。
gRPCのコネクションを確立して、コネクションをもとにgRPC Clientを作成します。gRPC Clientでサーバー側にコールしてUserを取得しています。

func main() {
    // Set up a gRPC client
    conn, err := grpc.Dial("localhost:5001", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewUsersClient(conn)

    // Request to gRPC server
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.FindByID(ctx, &pb.FindByIDRequest{
        Id: "1",
    })
    if err != nil {
        log.Fatalf("could not find user: %v", err)
    }
    log.Printf("User: %v", r.User)
}

動作確認

サーバーとクライアントの実装が完了したので動作確認をします。
ターミナルを2つ起動して、片方でサーバーを起動し、もう一方でクライアントを起動します。クライアントを起動するとサーバーにコールしてUser情報がターミナル上に表示されます。

# terminal 1
> go run server/main.go

# terminal 2
> go run client/main.go
2019/09/06 22:15:16 User: id:"1" name:"Sample"

まとめ

普段の開発はHTTPSで実装する場合が多く、RPCで実装することがあまりないので、今回はgRPCを利用して簡単なサーバーとクライアントを構築しました。
本記事ではgRPCの基本的な使い方を紹介しましたが、今後の記事ではもう少し複雑な実装に踏み込んだものを書いていきます!

参考文献

grpc.io

developers.google.com

ja.wikipedia.org