見出し画像

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"}

いいなと思ったら応援しよう!