概要
gRPCはProtocol Buffersを喋るもの同士ではメリットが大きいですが、RESTしか叩け無いものや、curlでRESTを叩くようにサクッと検証したかったりするときに不便です。
そんな問題を解消してくれるのがgrpc-gatewayで、protobuf定義からREST APIを提供するリバースプロキシを生成してくれます。
環境
成果物
今回のコードはこちらです。
実装
proto
死活監視用のサービスと、ユーザ情報に関するサービスの2つを用意してみます。
RESTのエンドポイントとしては
GET /alive
GET /user/{id}
GET /user
PUT /user/{id}
です。
alive.proto
syntax = "proto3"; import "google/api/annotations.proto"; package gateway; service AliveService { rpc GetStatus(Empty) returns (AliveResponse) { option (google.api.http) = { get: "/alive" }; } } message Empty {} message AliveResponse { bool status = 1; }
これまでのprotoとの違いとしては、
... import "google/api/annotations.proto"; ... option (google.api.http) = { get: "/alive" }; ...
この辺のコードが追加された点です。
最初のimportはcustom optionを使えるようにするためです。
そのcustom optionで、REST APIのエンドポイントを定義しています。今回だとGET /alive
というエンドポイントになります。
user.proto
syntax = "proto3"; import "google/api/annotations.proto"; import "alive.proto"; package gateway; service UserService { rpc GetUser(GetUserRequest) returns (UserResponse) { option (google.api.http) = { get: "/user/{id}" }; } rpc GetUsersByGroup(UserGroupRequest) returns (UsersResponse) { option (google.api.http) = { get: "/user" }; } rpc UpdateUser(UpdateUserRequest) returns (Empty) { option (google.api.http) = { put: "/user/{id}" body: "*" }; } } message GetUserRequest { string id = 1; } message UpdateUserRequest { string id = 1; string name = 2; int32 age = 3; } message UserResponse { string id = 1; string name = 2; int32 age = 3; } enum Group { USER = 0; ADMIN = 1; DEVELOPER = 2; } message UserGroupRequest { Group group = 1; } message UsersResponse { Group group = 1; repeated UserResponse users = 2; }
path parameter
こちらは先程の応用で、{id}
というpath parameterが追加されています。
... option (google.api.http) = { get: "/user/{id}" }; ...
path parameterの{id}
は、リクエストの定義で用意しなくてはいけません。今回だと以下のコードです。
message GetUserRequest { string id = 1; }
REST APIではbodyを付けずにGET /user/001
みたいにするだけなのですが、proto定義ではid
を含んだmessageを用意しなくてはいけません。
query string
query stringの場合も同様に、あらかじめmessageで定義しておきます。違いとしては↑のpath parameterのようにpathで使われないものが全てquery stringとして扱われます。
rpc GetUsersByGroup(UserGroupRequest) returns (UsersResponse) { option (google.api.http) = { get: "/user" }; } ... message UserGroupRequest { Group group = 1; }
としていると、/user?group=ADMIN
といった形で使えます。
今回はついでにenumも数値でなく文字列で扱っています。小文字にしたい場合はproto定義を小文字にしてください。
request body
これはrequest bodyを持ったPUTリクエストを定義しています。
... option (google.api.http) = { put: "/user/{id}" body: "*" }; ...
body: "*"
という行が含まれていますが、これはmessageのどの部分を(または全部を)bodyとして受け取るのかの指定です。
通常全フィールドを受け取るので"*"
としています。
コンパイル
今回もprotoeasyでさくっとやります。--grpc-gateway
というオプションを付けてください。
$ protoeasy --go \ --go-import-path=github.com/jun06t/grpc-sample/grpc-gateway/proto \ --grpc --grpc-gateway ./proto
するとalive.pb.gw.go
やuser.pb.gw.go
といったgateway用のコードが生成されます。
server
生成されたコードのインタフェースを実装します。
type aliveService struct{} func (s *aliveService) GetStatus(ctx context.Context, in *pb.Empty) (*pb.AliveResponse, error) { return &pb.AliveResponse{Status: true}, nil } type userService struct{} func (s *userService) GetUser(ctx context.Context, in *pb.GetUserRequest) (*pb.UserResponse, error) { return &pb.UserResponse{ Id: in.Id, Name: "Alice", Age: 20, }, nil } func (s *userService) UpdateUser(ctx context.Context, in *pb.UpdateUserRequest) (*pb.Empty, error) { log.Printf("update body is {id: %s, name: %s, age: %d}\n", in.Id, in.Name, in.Age) return &pb.Empty{}, nil }
分かりやすいようにpath parameterやrequest bodyをログに出してます。
それぞれサーバに登録します。
func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatal(err) } s := grpc.NewServer() pb.RegisterAliveServiceServer(s, new(aliveService)) pb.RegisterUserServiceServer(s, new(userService)) err = s.Serve(lis) if err != nil { log.Fatal(err) } }
gateway proxy
endpoint
にgRPCサーバのエンドポイントをセットします。
またalive.pb.gw.go
やuser.pb.gw.go
にある、RegisterXxxxHandler(ctx, mux, conn)
というメソッドを使ってmultiplexerに登録します。
func newGateway(ctx context.Context, opts ...runtime.ServeMuxOption) (http.Handler, error) { mux := runtime.NewServeMux(opts...) dialOpts := []grpc.DialOption{grpc.WithInsecure()} conn, err := grpc.Dial(endpoint, dialOpts...) if err != nil { return nil, err } err = pb.RegisterAliveServiceHandler(ctx, mux, conn) if err != nil { return nil, err } err = pb.RegisterUserServiceHandler(ctx, mux, conn) if err != nil { return nil, err } return mux, nil }
注意としてこの時のruntime
は標準パッケージでなく"github.com/grpc-ecosystem/grpc-gateway/runtime"
というgrpc-gatewayのパッケージです。
あとは通常のサーバのようにListenします。
func Run(address string, opts ...runtime.ServeMuxOption) error { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() gw, err := newGateway(ctx, opts...) if err != nil { return err } return http.ListenAndServe(address, gw) } func main() { if err := Run(":3000"); err != nil { panic(err) } }
動作確認
サーバを起動します。
$ go run server/main.go
次にgateway proxyを起動します。
$ go run gateway/main.go
GET /alive
curlでGET /alive
を叩いてみます。
$ curl http://localhost:3000/alive {"status":true}
GET /user/{id}
次にGET /user/{id}
を叩いてみます。
$ curl http://localhost:3000/user/100 {"id":"100","name":"Alice","age":20}
ちゃんとpath parameterが反映されてますね。
GET /user
次にGET /user?group=ADMIN
を叩いてみます。
curl http://localhost:3000/user?group=ADMIN {"group":"ADMIN","users":[{"name":"Alice","age":20},{"name":"Bob","age":24}]}
ちゃんとquery stringが反映されています。
PUT /user/{id}
最後にPUT /user/{id}
を叩いてみます。
$ curl -XPUT http://localhost:3000/user/100 -d '{"name": "bob", "age": 16}'
サーバ側のログは
2017/11/14 23:25:52 update body is {id: 100, name: bob, age: 16}
となりました。問題なくrequest bodyを送ることができました。