Chapter 05

gRPCサーバーを動かしてみよう

さき(H.Saki)
さき(H.Saki)
2022.06.19に更新

この章について

この章では、protoファイルから自動生成されたサーバーサイド用のコード

  • GreetingServiceServerインターフェース
  • RegisterGreetingServiceServer関数

を利用して、実際に動くgRPCサーバーを作ってみます。

gRPCサーバーの実装

ファイルの用意

今回、gRPCサーバーをcmd/serverディレクトリ直下に作っていきます。

./src
 ├─ api
 │   └─ hello.proto # protoファイル
+├─ cmd
+│   └─ server
+│       └─ main.go
 ├─ pkg
 │   └─ grpc # 自動生成されたコード
 │       ├─ hello.pb.go
 │       └─ hello_grpc.pb.go
 ├─ go.mod
 └─ go.sum

サーバーを起動する部分のコードを書く

まずは、gRPCサーバーをlocalhost:8080で動かすためのコードを書いてみます。

cmd/server/main.go
package main

import (
	// (一部抜粋)
	"google.golang.org/grpc"
)

func main() {
	// 1. 8080番portのLisnterを作成
	port := 8080
	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		panic(err)
	}

	// 2. gRPCサーバーを作成
	s := grpc.NewServer()

	// 3. 作成したgRPCサーバーを、8080番ポートで稼働させる
	go func() {
		log.Printf("start gRPC server port: %v", port)
		s.Serve(listener)
	}()

	// 4.Ctrl+Cが入力されたらGraceful shutdownされるようにする
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("stopping gRPC server...")
	s.GracefulStop()
}

grpc.Server

HTTPサーバーに対応するのがnet/httpパッケージのhttp.Server型なように、gRPCサーバーに対応する型がgoogle.golang.org/grpcパッケージに用意されています。

// google.golang.org/grpcパッケージ
type Server struct {
	// contains filtered or unexported fields
}
func NewServer(opt ...ServerOption) *Server

そのコンストラクタであるgrpc.NewServer関数を呼び出すことで、今回使うgRPCサーバーを用意しています。

cmd/server/main.go
// 2. gRPCサーバーを作成
s := grpc.NewServer()

gRPCサーバーにサービスを登録

今のままでは、gRPCサーバーに何のエンドポイント・機能も実装されていません。
いうならば、ハンドラが一切登録されていないHTTPサーバーのようなものです。

func main() {
	// ハンドラの登録なしに
	// http.HandleFunc("/", myHandler)

	// サーバーを起動させているようなもの
	log.Fatal(http.ListenAndServe(":8080", nil))
}

protoファイルで定義したサービスGreetingServiceをgRPCサーバー上で動かすためには、「gRPCサーバーにサービスを登録する」作業が必要になります。
そして「サービスGreetingServiceをgRPCサーバーに登録するための関数」というのは、実は既に自動生成されて存在しています。
それがhello_grpc.pb.goの中にできていたRegisterGreetingServiceServer関数です。

pkg/grpc/hello_grpc.pb.go
func RegisterGreetingServiceServer(s grpc.ServiceRegistrar, srv GreetingServiceServer)

この関数をmain関数の中で使って、GreetingServiceサービスをgRPCサーバーに登録しましょう。

cmd/server/main.go
package main

import (
	// (一部抜粋)
	"google.golang.org/grpc"
+	hellopb "mygrpc/pkg/grpc"
)

func main() {
	// 1. 8080番portのLisnterを作成
	// (略)

	// 2. gRPCサーバーを作成
	s := grpc.NewServer()

+	// 3. gRPCサーバーにGreetingServiceを登録
+	hellopb.RegisterGreetingServiceServer(s, [サーバーに登録するサービス])

	// 4. 作成したgRPCサーバーを、8080番ポートで稼働させる
	go func() {
		log.Printf("start gRPC server port: %v", port)
		s.Serve(listener)
	}()

	// 5.Ctrl+Cが入力されたらGraceful shutdownされるようにする
	// (略)
}

サービスの実態を作成する

ここで、RegisterGreetingServiceServer関数に渡す第二引数がまだできていないということにお気づきの方もいらっしゃるかと思います。

cmd/server/main.go
// 3. gRPCサーバーにGreetingServiceを登録
hellopb.RegisterGreetingServiceServer(s, [サーバーに登録するサービス])

この第二引数の型はGreetingServiceServerインターフェースで、HelloRequest型を受け取ってHelloResponse型を返却するHelloメソッドを持っています。

pkg/grpc/hello_grpc.pb.go
// RegisterGreetingServiceServer関数の定義
// -> 第二引数はGreetingServiceServerインターフェース型
func RegisterGreetingServiceServer(s grpc.ServiceRegistrar, srv GreetingServiceServer)

// GreetingServiceServerインターフェース型の定義
type GreetingServiceServer interface {
	// Helloメソッドを持つ
	Hello(context.Context, *HelloRequest) (*HelloResponse, error)
	mustEmbedUnimplementedGreetingServiceServer()
}

つまり、RegisterGreetingServiceServer関数の第二引数には、Helloメソッド(とmustEmbedUnimplementedGreetingServiceServerメソッド)を持つ構造体ならば代入することができるのです。

そのため、これから私たちは第二引数に代入できる自作構造体を定義します。
そしてそこに「受け取ったHelloRequest型から、どのような処理を経てレスポンスであるHelloResponse型を作るのか」というビジネスロジックを含んだHelloメソッドを実装していきます。

自作サービス構造体の定義

さっそく自作サービス構造体を定義しましょう。

cmd/server/main.go
type myServer struct {
	hellopb.UnimplementedGreetingServiceServer
}

このmyServer型に、これからサービスに必要なHelloメソッドを実装していきます。

サービスメソッドの実装

それでは、サービスで定義された「HelloRequest型のリクエストを受け取って、HelloResponse型のレスポンスを返す」Helloメソッドを作っていきましょう。

cmd/server/main.go
func (s *myServer) Hello(ctx context.Context, req *hellopb.HelloRequest) (*hellopb.HelloResponse, error) {
	// リクエストからnameフィールドを取り出して
	// "Hello, [名前]!"というレスポンスを返す
	return &hellopb.HelloResponse{
		Message: fmt.Sprintf("Hello, %s!", req.GetName()),
	}, nil
}

自作サービス構造体をgRPCサーバーに登録

Helloメソッドを実装した自作サービス構造体myServer型ができたところで、今度こそこれをgRPCサーバーに登録しましょう。

cmd/server/main.go
+// 自作サービス構造体のコンストラクタを定義
+func NewMyServer() *myServer {
+	return &myServer{}
+}

func main() {
	// 1. 8080番portのLisnterを作成
	// (略)

	// 2. gRPCサーバーを作成
	s := grpc.NewServer()

	// 3. gRPCサーバーにGreetingServiceを登録
-	hellopb.RegisterGreetingServiceServer(s, [サーバーに登録するサービス])
+	hellopb.RegisterGreetingServiceServer(s, NewMyServer())

	// 4. 作成したgRPCサーバーを、8080番ポートで稼働させる
	go func() {
		log.Printf("start gRPC server port: %v", port)
		s.Serve(listener)
	}()

	// 5.Ctrl+Cが入力されたらGraceful shutdownされるようにする
	// (略)
}

サーバーを起動して動作確認をしてみよう

サーバーの実装ができたところで、早速それを動かして動作確認を行なっていきましょう。

gRPCurlのインストール

まずは、gRPCurl[1]というツールのインストールを行います。
これを用いることで、curlコマンドのようにターミナル上でgRPCのリクエストを送ることができるようになります。

$ brew install grpcurl
$ which grpcurl
[パスが表示されればインストール成功]

サーバーリフレクションの設定

gRPCurlを使うためには、リクエストを送るgRPCサーバーに「サーバーリフレクション」という設定がなされていることが前提となります。
そのため、その設定をコードの中に追加します。

cmd/server/main.go
import (
	// (一部抜粋)
	"google.golang.org/grpc"
+	"google.golang.org/grpc/reflection"
	hellopb "mygrpc/pkg/grpc"
)

func main() {
	// 1. 8080番portのLisnterを作成
	// (略)

	// 2. gRPCサーバーを作成
	s := grpc.NewServer()

	// 3. gRPCサーバーにGreetingServiceを登録
	hellopb.RegisterGreetingServiceServer(s, [サーバーに登録するサービス])

+	// 4. サーバーリフレクションの設定
+	reflection.Register(s)

	// 5. 作成したgRPCサーバーを、8080番ポートで稼働させる
	go func() {
		log.Printf("start gRPC server port: %v", port)
		s.Serve(listener)
	}()

	// 6.Ctrl+Cが入力されたらGraceful shutdownされるようにする
	// (略)
}

[コラム]サーバーリフレクションとは?

gRPCの通信はProtocol Bufferでシリアライズされているという話を2章で書きました。
そして実は、そのシリアライズ・デシリアライズを行うためには、protoファイルによって書かれた「シリアライズのルール」を知る必要があります。

この後6章で紹介するgRPCクライアントは、サーバーファイルと同じくprotocコマンドから自動生成されたコードを使用して作るため、その「シリアライズのルール」が既に組み込まれているのですが、gRPCurlコマンドは違います。
元からprotoファイルによるメッセージ型の定義を知らないgRPCurlコマンドは、代わりに「gRPCサーバーそのものから、protoファイルの情報を取得する」ことで「シリアライズのルール」を知り通信します。
そしてその「gRPCサーバーそのものから、protoファイルの情報を取得する」ための機能がサーバーリフレクションです。

詳細は以下の公式GitHubの資料をご覧ください。

https://github.com/grpc/grpc/blob/master/doc/server-reflection.md

動作確認

これで、gRPCurlを用いてリクエストを送る準備が整いました。

サーバーの起動

まずはmain.goを実行してサーバーを起動してみましょう。

$ cd cmd/server
$ go run main.go
2022/04/16 17:22:00 start gRPC server port: 8080

このような起動ログが出れば正常に動いています。

サーバー内に実装されているサービス一覧の確認

それではリクエストを送ってみましょう。
まずはgRPCサーバーにどんなサービスが稼働しているのかを確認します。

$ grpcurl -plaintext localhost:8080 list
grpc.reflection.v1alpha.ServerReflection
myapp.GreetingService

これで、リクエストを送ったgRPCサーバーには、サーバーリフレクション用のサービスgrpc.reflection.v1alpha.ServerReflectionと、protoファイルで定義した上で先ほど自ら実装したサービスmyapp.GreetingServiceの2つが稼働していることがわかりました。

あるサービスのメソッド一覧の確認

次に、GreetingServiceサービスが持つメソッド一覧を見てみましょう。

$ grpcurl -plaintext localhost:8080 list myapp.GreetingService
myapp.GreetingService.Hello

Helloメソッドの存在が確認できました。

メソッドの呼び出し

それでは最後にHelloメソッドにリクエストを送ってみます。
引数として渡すメッセージ型を、-dオプションを使って指定します。

$ grpcurl -plaintext -d '{"name": "hsaki"}' localhost:8080 myapp.GreetingService.Hello
{
  "message": "Hello, hsaki!"
}

hsakiというnameフィールドを含めたリクエストに対して、Hello, hsaki!というレスポンスを受け取ることができました。

おめでとうございます。これにて初めてのgRPCサーバーを動かすことができました!

脚注
  1. https://github.com/fullstorydev/grpcurl ↩︎