25
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

grpc-gatewayでgRPCのREST対応を試しました

Last updated at Posted at 2019-03-20

はじめに

初めまして、k.s.ロジャースの西谷です。

自分自身はPHPで開発することが多いですが、他のプロジェクトではGoを利用しており、その中でREST APIからgRPCに移行する話が出てきています。
しかし、過渡期ということもあり、REST APIとgRPC両方の対応をする方針で進んでいます。
この場合はRESTとgRPCの実装を分けてしまうと、実装時も大変ですが、リリース後の改修等のメンテナンスコストも大きく上がるため、良くないと考えています。

そこで、grpc-gatewayを使えばgRPCをREST APIで叩けると聞いたため、今回検証をしてみようと思います。
今回は全体の流れを大まかに記載し、次回以降各部位の詳細について記載出来たらと思います。

間違い・助言等があればコメントにてお知らせいただけたらと思います。

環境構築

環境構築については他の記事にお任せしたいと思います。
本検証のために以下の記事が非常に参考になりました。
https://budougumi0617.github.io/2018/02/03/grpc-gateway-for-rest-api/
https://christina04.hatenablog.com/entry/2017/11/15/034455

protbufでAPIの定義

今回はサンプルとしてユーザのCRUDを作ろうと思います。
つまり次のAPIを作成する必要があります。

Method URL 役割
ListUsers GET /api/v1/users ユーザ一覧取得
GetUser GET /api/v1/users/{encrypted_id} 1ユーザを取得
CreateUser POST /api/v1/users ユーザ作成
UpdateUser PUT /api/v1/users/{encrypted_id} ユーザ更新
DeleteUser DELET /api/v1/users/{encrypted_id} ユーザ削除

そこで以下のprotoファイルを定義しました。

sample.proto
syntax = "proto3";

package sample;

import "google/api/annotations.proto";

service UserService {
    rpc ListUsers (ListUserRequest) returns (ListUsersResponses) {
        option (google.api.http) = {
          get: "/api/v1/users"
        };
    };

    rpc GetUser (GetUserRequest) returns (User) {
        option (google.api.http) = {
          get: "/api/v1/users/{encrypted_id}"
        };
    };

    rpc CreateUser (CreateUserRequest) returns (User) {
        option (google.api.http) = {
          post: "/api/v1/users"
          body: "*"
        };
    };

    rpc UpdateUser (UpdateUserRequest) returns (User) {
        option (google.api.http) = {
          put: "/api/v1/users/{encrypted_id}"
          body: "*"
        };
    };

    rpc DeleteUser (DeleteUserRequest) returns (Empty) {
        option (google.api.http) = {
          delete: "/api/v1/users/{encrypted_id}"
        };
    };
}

message Empty {
}

message ListUserRequest {
}

message GetUserRequest {
    string encrypted_id = 1;
}

message CreateUserRequest {
    string name = 1;
}

message UpdateUserRequest {
    string encrypted_id = 1;
    string name = 2;
}

message DeleteUserRequest {
    string encrypted_id = 1;
}

message User {
    string encrypted_id = 1;
    string name = 2;
}

message ListUsersResponses {
    repeated User users = 1;
}

上記ファイル作成後、以下コマンドでgRPC用のコードとgatewayのコードが生成されます。

protoc -I ./proto ./proto/sample.proto \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:./proto \
--go_out=plugins=grpc:proto

gRPCサーバの実装

サーバはgolangで実装しました。
本来はDB接続を行いますが、今回はAPIの動作確認が目的のため変数で代用しました:bow:
また、エラー処理も一切含まれていません。

server/main.go
package main

import (
	"flag"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	pb "grpc-gateway/proto"
	"log"
	"math/rand"
	"net"
	"time"
)

// DBの代わり(手抜きすみません)
var USERS = map[string]string{"12345abcde": "hogehoge", "zxcvb09876": "fugafuga"}

type userService struct{}

// ユーザ一覧取得
func (e *userService) ListUsers(ctx context.Context, req *pb.ListUserRequest) (*pb.ListUsersResponses, error) {
	var users = []*pb.User{}
	for k, v := range USERS {
		users = append(users, &pb.User{EncryptedId: k, Name: v})
	}

	return &pb.ListUsersResponses{Users: users}, nil
}

// 1ユーザの取得(本来は詳細情報を付与して返す)
func (e *userService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
	return &pb.User{EncryptedId: req.EncryptedId, Name: USERS[req.EncryptedId]}, nil
}

// ユーザの作成
func (e *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
	encrypedId := GetRandString(10)
	USERS[encrypedId] = req.Name

	return &pb.User{EncryptedId: encrypedId, Name: req.Name}, nil
}

// ユーザの更新
func (e *userService) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.User, error) {
	USERS[req.EncryptedId] = req.Name

	return &pb.User{EncryptedId: req.EncryptedId, Name: req.Name}, nil
}

// ユーザ削除
func (e *userService) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*pb.Empty, error) {
	delete(USERS, req.EncryptedId)
	return &pb.Empty{}, nil
}

// 乱数の生成(encrypted_id用)
func GetRandString(n int) string {
	var letterRunes = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

	b := make([]rune, n)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func main() {
	rand.Seed(time.Now().UnixNano())

	var port string
	flag.StringVar(&port, "port", "50000", "")
	flag.Parse()

	listen, err := net.Listen("tcp", ":"+port)
	if err != nil {
		log.Fatalln(err)
	}

	server := grpc.NewServer()
	pb.RegisterUserServiceServer(server, &userService{})
	reflection.Register(server)

	if err := server.Serve(listen); err != nil {
		log.Fatalln(err)
	}
}

go run server/main.goなどのコマンドで実行できます。

gRPCクライアント

REST APIの前にgRPCのクライアントを実装して動作確認します。

client/main.go
package main

import (
	"bufio"
	"flag"
	"fmt"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"log"
	"os"
	"time"

	pb "grpc-gateway/proto"
)

// 全ユーザ取得
func ListUser(conn *grpc.ClientConn)  {
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.ListUsers(ctx, &pb.ListUserRequest{	})
	if err != nil {
		log.Fatalln(err)
	}

	for _, v := range resp.Users {
		log.Printf("EncryptedId: %s, Name: %s\n", v.EncryptedId, v.Name)
	}
}

// 1ユーザ取得
func GetUser(conn *grpc.ClientConn, encryptedId string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.GetUser(ctx, &pb.GetUserRequest{EncryptedId:encryptedId})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("EncryptedId: %s, Name: %s\n", resp.EncryptedId, resp.Name)
}

// ユーザ作成
func CreatUser(conn *grpc.ClientConn, name string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.CreateUser(ctx, &pb.CreateUserRequest{Name:name})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("EncryptedId: %s, Name: %s\n", resp.EncryptedId, resp.Name)
}

// ユーザ更新
func UpdateUser(conn *grpc.ClientConn, encryptedId string,name string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	resp, err := client.UpdateUser(ctx, &pb.UpdateUserRequest{EncryptedId:encryptedId, Name:name})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("EncryptedId: %s, Name: %s\n", resp.EncryptedId, resp.Name)
}

// ユーザ削除
func DeleteUser(conn *grpc.ClientConn, encryptedId string){
	client := pb.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	_, err := client.DeleteUser(ctx, &pb.DeleteUserRequest{EncryptedId:encryptedId})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("deleted")
}

// 標準入力受付
func GetStdInput(message string) string {
	fmt.Printf(message)

	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()

	return scanner.Text()
}

func main() {
	var addr string
	flag.StringVar(&addr, "addr", "localhost:50000", "")
	flag.Parse()

	conn, err := grpc.Dial(addr, grpc.WithInsecure())
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()

L:
	for true {
		cmd := GetStdInput("Input ListUsers/GetUser/CreateUser/UpdateUser/DeleteUser/exit\n")

		switch cmd {
		case "ListUsers":
			ListUser(conn)
		case "GetUser":
			encryptedId := GetStdInput("encryptedId:")
			GetUser(conn, encryptedId)
		case "CreateUser":
			name := GetStdInput("name:")
			CreatUser(conn, name)
		case "UpdateUser":
			encryptedId := GetStdInput("encryptedId:")
			name := GetStdInput("name:")
			UpdateUser(conn, encryptedId, name)
		case "DeleteUser":
			encryptedId := GetStdInput("encryptedId:")
			DeleteUser(conn, encryptedId)
		case "exit":
			break L
		}
	}
}

go run client/main.goなどのコマンドで実行できます。
次のように対話形式でAPIを実行出来ると思います。
(データは内部変数でしか保持していないため、サーバを終了するとリセットされます)
スクリーンショット 2019-03-20 2.49.00.png

REST API対応

こちらは表題のgrpc-gatewayを利用すれば簡単に出来ました。

gateway/main.go
package main

import (
	"flag"
	"net/http"

	"github.com/golang/glog"
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	pb "grpc-gateway/proto"
)

var (
	userServiceEndpoint = flag.String("user_service_endpoint", "localhost:50000", "Users sample")
)

func run() error {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
	err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, *userServiceEndpoint, opts)
	if err != nil {
		return err
	}

	return http.ListenAndServe(":50001", mux)
}

func main() {
	flag.Parse()
	defer glog.Flush()

	if err := run(); err != nil {
		glog.Fatal(err)
	}
}

実装はREADMEの「Write an entrypoint」を数行変更するくらいで、むしろgRPCのサーバとクライアントの実装が余程時間がかかりました。

サンプルの変更箇所
(省略)

// 各自のprotoを参照するように変更
-  gw "path/to/your_service_package"
+  pb "grpc-gateway/proto"

(省略)

var (
// ポートと名前を変更
-  echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService")
+  userServiceEndpoint = flag.String("user_service_endpoint", "localhost:50000", "Users sample")
)

(省略)

  mux := runtime.NewServeMux()
  opts := []grpc.DialOption{grpc.WithInsecure()}
// protobufから生成したサービスに変更
-  err := gw.RegisterYourServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts)
+  err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, *userServiceEndpoint, opts)
  if err != nil {
    return err
  }

go run gateway/main.goなどのコマンドで実行できます。
serverとgatewayが実行されている状態でREAT APIを実行できます。
テストクライアントはinsomniaを利用しました。

  • 新規ユーザの追加
スクリーンショット 2019-03-20 3.08.20.png
  • ユーザ一覧取得
スクリーンショット 2019-03-20 3.08.54.png

終わりに

今回はgrpc-gatewayを用いたREST対応について説明しました。
検証の意味合いが強く、説明が不十分な箇所は多々あると思います。
需要があれば、次回以降でgRPCの最初から詳しく調査して記事にしていこうと思います。

Wantedlyでもブログ投稿してます

Techブログに加えて会社ブログなどもやっているので、気になった方はぜひ覗いてみてください。
https://www.wantedly.com/companies/ks-rogers

25
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?