gRPC/Go で簡単なサーバーとクライアントを実装する
はじめに
本記事ではgRPCを利用したサーバーとクライアントを実装します。マイクロサービス開発などでよく使われるgRPCですが、ちゃんと調べたことがなかったので簡単なサーバとクライアントを実装してまとめました。
今回の例として使用したリポジトリはGitHubにあります。
- はじめに
- gRPCとは
- RPC(遠隔手続き呼び出し)とは
- Protocol Buffersとは
- gRPCを使用してサーバーとクライアントを実装する
- 準備
- Protocol Buffersによるインタフェースとメッセージの定義
- Serverの実装
- Clientの実装
- 動作確認
- まとめ
- 参考文献
gRPCとは
gRPCはGoogleによって開発されたリモートプロシージャコールシステム(RPC System)です。 gRPCを使用するとクライアントがサーバーのメソッドを直接呼び出すことができます。マイクロサービスでは多くのサービスが存在し、それぞれのサービスを利用する必要があります。そこでgRPCを使用すると他のマイクロサービスのメソッドを容易に呼び出すことが可能になるという利点があります。
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として定義されており、後述するFindByIDRequest
とFindByIDResponse
を引数と戻り値に定義しています。FindByID
は、サーバー側で実態が実装され、クライアント側からコールしてレスポンスを取得する時のインターフェースとなります。
service Users { rpc FindByID (FindByIDRequest) returns (FindByIDResponse) {} }
リクエストとレスポンスのメッセージの構造を定義する
リクエストとレスポンスは以下のようになります。FindByIDRequest
はUser
のIDのみを定義しています。FindByIDResponse
はUser
を所持しています。
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の基本的な使い方を紹介しましたが、今後の記事ではもう少し複雑な実装に踏み込んだものを書いていきます!