株式会社Schoo 新卒1年目の @hiroto_0411です!
最近、gRPCサーバーの実装を行っていく中で、grpc-gatewayというのがあると知ったので実装しながら理解を深めてみました!
この記事でわかること
- grpc-gatewayってなに?
- grpc-gatewayを使った、gRPCサーバーを作る流れ
gRPCについて理解が曖昧な方は以下の記事にまとめたのでこちらを先に見ていただくと、この記事が理解しやすくなると思います。
grpc-gatewayとは
.protoファイルを元に、REST APIに送られてきたリクエストを変換して、gRPCサーバーに転送するためのリバースプロキシーサーバーを作ることができるプラグイン。
リバースプロキシーサーバー
クライアントからのリクエストを別のサーバーに転送するサーバー
イメージ
grpc-gatewayでリバースプロキシーサーバーを作ることで、REST APIとgRPC APIどちらも実装できる。
後方互換性の維持、gRPCで十分にサポートされていない言語やクライアントのサポート、RESTの思想の方が合っているなど、gRPCだけでなくREST APIも提供したいとケースに対応するために作られた。
参考
grpc-gatewayを使ってみる
上記のREADMEを参考にしながらgrpc-gatewayを使ってみた。
作成したコードは以下で見られる。
ディレクトリ構成はこんな感じになる予定。
.
├── gateway
│ └── main.go
├── gen
│ ├── helloworld_grpc.pb.go
│ ├── helloworld.pb.go
│ └── helloworld.pb.gw.go
├── proto
│ └── helloworld.proto
├── server
│ └── helloworld.go
├── buf.gen.yaml
├── buf.lock
├── buf.yaml
├── go.mod
├── go.sum
└── main.go
1. 準備
モジュールの作成
go mod init github.com/user-name/project-name
コード生成に必要なツールのインストール
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/bufbuild/buf/cmd/buf@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
PATHを更新
export PATH="$PATH:$(go env GOPATH)/bin"
2. .protoファイルの作成
Protocol Buffersというフォーマットを使用してgRPCのサービス名やメソッド名、リクエストやレスポンスを定義する。
syntax = "proto3";
option go_package = "github.com/user-name/project-name/gen";
import "google/api/annotations.proto";
package hello;
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse){
option (google.api.http) = {
post: "/hello"
body: "*"
};
}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
この後、.protoファイルを元にコードを自動生成する。その時に出力したいpackage名をgo_package
で指定する。package hello;
はGoのpackage名とは関係ない。このpackageは名前が衝突するのを防ぐために使われる。
package user;
message Person {
string name = 1;
int32 age = 2;
}
package employee;
message Person {
string name = 1;
string employee_id = 2;
}
このように同じ名前のserviceやmessageを定義したときに問題が発生しないように、package名を指定する。
3. コードの自動生成
buf.yamlファイルをルートディレクトリに作成
buf.yamlにはどのディレクトリの.protoファイルをコンパイルするかや、linterなどの設定、リモートのプロジェクトやサードパーティのProtobufファイルを参照するための依存関係の設定などを記述する。
buf config init
# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
use:
- STANDARD
breaking:
use:
- FILE
deps:
- buf.build/googleapis/googleapis
./proto/helloworld.proto
でimport "google/api/annotations.proto";
している依存関係を記述する。
依存関係のロックファイルを更新
初めてこのコマンドを実行するときはbuf.lockが作成される。
buf mod update
buf.gen.yamlをルートディレクトリに作成
version: v2
plugins:
- local: protoc-gen-go
out: gen
opt:
- module=github.com/user-name/project-name/gen
- local: protoc-gen-go-grpc
out: gen
opt:
- module=github.com/user-name/project-name/gen
- local: protoc-gen-grpc-gateway
out: gen
opt:
- module=github.com/user-name/project-name/gen
- generate_unbound_methods=true
コードを自動生成
以下のコマンドを実行することで、.protoファイルを元に、XXX_grpc.pg.goとXXX.pb.go、XXX.pb.gw.goの3種類のファイルが自動生成される。
buf generate
モジュールの依存関係を整理
go mod tidy
4. 自動生成されたinterfaceを使い、gRPCサーバーを実装する
type GreeterServer interface {
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
mustEmbedUnimplementedGreeterServer()
}
type UnimplementedGreeterServer struct{}
func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
package server
import (
"context"
"log"
pb "github.com/user-name/project-name/gen"
)
type GreeterServer struct {
pb.UnimplementedGreeterServer
}
// ./gen/helloworld_grpc.pb.goに定義されているGreeterServerインターフェースを実装する
func (s *GreeterServer) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
log.Printf("Received: %v", in.GetName())
// 本来ならここから、別の層のメソッドを呼び出すなどする
return &pb.HelloResponse{Message: "Hello " + in.GetName()}, nil
}
./gen/helloworld_grpc.pb.goに定義されているGreeterServerインターフェースを実装するメソッドを作成し、それをmain.goで使用する。
package main
import (
"flag"
"fmt"
"log"
"net"
pb "github.com/user-name/project-name/gen"
"github.com/user-name/project-name/server"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
var (
port = flag.Int("port", 50051, "The server port")
)
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server.GreeterServer{})
log.Printf("server listening at %v", lis.Addr())
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
5. gatewayサーバーを実装する
package main
import (
"context"
"flag"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/grpclog"
gw "github.com/user-name/project-name/gen"
)
var (
// command-line options:
// gRPC server endpoint
grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:50051", "gRPC server endpoint")
)
func run() error {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Register gRPC server endpoint
// Note: Make sure the gRPC server is running properly and accessible
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := gw.RegisterGreeterHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
if err != nil {
return err
}
// Start HTTP server (and proxy calls to gRPC server endpoint)
return http.ListenAndServe(":8081", mux)
}
func main() {
flag.Parse()
if err := run(); err != nil {
grpclog.Fatal(err)
}
}
6. それぞれのサーバーにリクエストを送ってみる
サーバーの起動
それぞれのmain.goがあるディレクトリに移動して起動する。
go run main.go
grpcurlをインストール
brew install grpcurl
gRPCサーバーにリクエストを送信
grpcurl -plaintext -d '{"name": "パブりん"}' localhost:50051 hello.Greeter/SayHello
{
"message": "Hello パブりん"
}
gatewayサーバーにリクエストを送信
curl \
--header "Content-Type: application/json" \
--data '{"name": "パブりん"}' \
http://localhost:8081/hello
{
"message": "Hello パブりん"
}
参考
Schooでは一緒に働く仲間を募集しています!