gRPC-Gatewayを使ってサーバと通信する
grpc-ecosystem/grpc-gatewayのREADMEを参考にして自分なりに理解できるようにまとめています。
全ソースはこちらにあげてます。
依存関係の管理にGoモジュールを使用していることを前提としています。
フォルダ構成
grpc-gateway
|- api
| |- main.go
|- client
| |- main.go
|- proto
| |- test.proto
|- pb
|- gen
| |- go
| | proto
|- gateway
| |- main.go
|- tools
| |- tools.go
|- google
| |- api
|- go.mod
toolsモジュールの作成
gRPC-gatewayを使うためのモジュールを使えるようにします
# tools/tools.go
// +build tools
package tools
import (
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
)
*go mod tidyだけでなくgo installで該当モジュールをインストールしないとGOPATH参照できなかったため以下インストールします。
$ go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
作成するgRPCサービスのprotoファイルを定義します。
ここでは送ったメッセージを返却するだけのEchoサービスを作成します。
# proto/test.proto
syntax = "proto3";
package echo;
option go_package = "./pb";
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse);
}
gRPCスタブを作成
プロトコルバッファをコンパイルします
$ protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. ./proto/test.proto
pbディレクトリにgrpc.pb.go、pb.goファイルが生成されていればok
gRPCサーバの疎通確認
一旦gRPCサーバが正常動作するかどうかgrpc-gateway作成前に確認するため、クライアントスタブとサーバを作成します。
# api/main.go
package main
import (
"context"
"log"
"net"
pb "github.com/leslesnoa/grpc-gateway/pb"
"google.golang.org/grpc"
)
type EchoService struct {
}
func (s *EchoService) Echo(ctx context.Context, message *pb.EchoRequest) (*pb.EchoResponse, error) {
// log.Println(message)
log.Printf("Received: %v", message.Message)
// time.Sleep(3 * time.Second)
return &pb.EchoResponse{Message: "Hello " + message.Message}, nil
}
func main() {
addr := ":9090"
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterEchoServiceServer(s, &EchoService{})
log.Printf("gRPC server listening on " + addr)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
# client/main.go
package main
import (
"context"
"log"
"os"
pb "github.com/leslesnoa/grpc-gateway/pb"
"google.golang.org/grpc"
)
func main() {
addr := "localhost:5000"
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewEchoServiceClient(conn)
name := os.Args[1]
ctx := context.Background()
r, err := c.Echo(ctx, &pb.EchoRequest{Message: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
apiとclientのmain.goを起動してメッセージが返って来ればok
$ go run api/main.go
$ go run client/main.go test
gRPC-Gatewayの作成
ここから、前述したprotoファイルを追加の変更を加えてgRPC-Gatewayを作成していきます。
*元のprotoファイルの定義を変更しないで実装する方法もgithubのREADMEに記載がありますがここではprotoファイルに追加する方法で実装します。
syntax = "proto3";
package your.service.v1;
option go_package = "github.com/yourorg/yourprotos/gen/go/your/service/v1";
+
+import "google/api/annotations.proto";
+
message StringMessage {
string value = 1;
}
service YourService {
- rpc Echo(StringMessage) returns (StringMessage) {}
+ rpc Echo(StringMessage) returns (StringMessage) {
+ option (google.api.http) = {
+ post: "/v1/example/echo"
+ body: "*"
+ };
+ }
}
protoファイルを上記のように変更する必要があるそうなので今回の実装に当てはめて作成します。
# proto/test.proto
syntax = "proto3";
package echo;
option go_package = "./pb";
import "google/api/annotations.proto";
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse) {
option (google.api.http) = {
get: "/echo"
};
}
}
grpc-gatewayは GET http://xxxxx/echo のhttpリクエストをEchoServiceにプロキシして送ってくれます。
grpc-gateway用に追加したプロトコルバッファをコンパイルします。
が、エラーが出てコンパイルできませんでした。READMEには以下の記述がありました。
protocスタブの生成にを使用している場合は、コンパイル時に必要な依存関係がコンパイラーで使用可能であることを確認する必要があります。これらは、googleapisリポジトリから関連ファイルを手動で複製してコピーしprotoc、実行時にそれらを提供することで見つけることができます
google/api/annotations.proto
google/api/field_behaviour.proto
google/api/http.proto
google/api/httpbody.proto
ということで上記のprotoファイルをリポジトリからローカルのgoogle/apiディレクトリにコピーしてきます。
その上で以下コマンドを実行します。
$ protoc -I . --grpc-gateway_out ./gen/go \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt paths=source_relative \
./proto/test.proto
pb.gw.goファイルが生成できていればok
ここまできたらgrpc-gatewayを作成していきます
# gateway/main.go
package main
import (
"context"
"flag"
"net/http"
"github.com/golang/glog"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
gw "github.com/leslesnoa/grpc-gateway/gen/go/proto"
)
var (
// command-line options:
// gRPC server endpoint
grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:9090", "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.WithInsecure()}
err := gw.RegisterEchoServiceHandlerFromEndpoint(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()
defer glog.Flush()
if err := run(); err != nil {
glog.Fatal(err)
}
}
ここでは8081ポートでgrpc-gatewayがHTTPリクエストを受けて、9090ポートのgRPCサーバへプロキシしています。
grpc-gatewayの動作確認
gRPCサーバとgrpc-gateway(HTTPリバースプロキシサーバ)を起動します。
$ go run api/main.go
$ go run gateway/main.go
curlでgrpc-gateway宛にリクエスト送ってメッセージが返答されたらok!
curl -XGET "localhost:8081/echo?message=World"
{"message":"Hello World"}