ではgRPCサーバーを実装していきましょう。
server.go
server.go
ファイルを作成し、server
構造体を定義します。この構造体にサービスのインターフェイスを実装していきます。
package main
type server struct{}
エンドポイントの実装
定義すべきインターフェイスは昨日生成したservice.pb.go
に定義されている以下の3つです。
type TodoAPIServer interface {
GetTodo(context.Context, *GetTodoRequest) (*GetTodoResponse, error)
CreateTodo(context.Context, *CreateTodoRequest) (*CreateTodoResponse, error)
ListTodos(context.Context, *ListTodosRequest) (*ListTodosResponse, error)
}
このパッケージをインポートし、メソッドを定義しましょう。
package main
import (
"context"
"github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service"
)
var _ service.TodoAPIServer = (*server)(nil)
type server struct{}
func (*server) GetTodo(context.Context, *service.GetTodoRequest) (*service.GetTodoResponse, error) {
return &service.GetTodoResponse{}, nil
}
func (*server) CreateTodo(context.Context, *service.CreateTodoRequest) (*service.CreateTodoResponse, error) {
return &service.CreateTodoResponse{}, nil
}
func (*server) ListTodos(context.Context, *service.ListTodosRequest) (*service.ListTodosResponse, error) {
return &service.ListTodosResponse{}, nil
}
ためしに、GetTodoではダミーのデータを返すようにしてみます。
func (*server) GetTodo(context.Context, *service.GetTodoRequest) (*service.GetTodoResponse, error) {
return &service.GetTodoResponse{
Todo: &service.Todo{
Title: "clean your desk!",
Description: "clean up the piles of documents from the desk to make more space.",
},
}, nil
}
サーバープロセスの立ち上げ
main.go
ファイルに戻り、gRPCサーバープロセスを新たなgoroutineとして起動します。
デフォルトで8090ポートを使います。
func main() {
app := cli.App(appName, appDesc)
...
grpcPort := app.Int(cli.IntOpt{
Name: "grpc-port",
Desc: "gRPC server port",
EnvVar: "GRPC_PORT",
Value: 8090,
})
app.Action = func() {
...
lis, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(*grpcPort)))
if err != nil {
log.Fatalln("init gRPC server:", err)
}
defer lis.Close()
gSrv := initialiseGRPCServer(&server{})
errCh := make(chan error, 2) // <- 1 incr.
...
go func() {
if err := gSrv.Serve(lis); err != nil {
errCh <- errors.Wrap(err, "gRPC server")
}
}()
...
}
if err := app.Run(os.Args); err != nil {
log.WithError(err).Fatal("app run")
}
}
ここで、initialiseGRPCServer()
関数は以下のようになっています。
func initialiseGRPCServer(srv service.TodoAPIServer) *grpc.Server {
gSrv := grpc.NewServer()
service.RegisterTodoAPIServer(gSrv, srv)
return gSrv
}
Gracefulなサーバーのシャットダウン
一応サーバープロセスを開始することができましたが、このままでは異常発生時にリクエストを捌く途中のままシャットダウンしてしまうことになるので、丁寧にシャットダウンするように改善してみます。
やることは2つ、
- 異常発生時に
GracefulStop()
メソッドを呼び出す -
sync.WaitGroup
を使って、サーバーの終了を待ち受ける
です。
func main() {
...
app.Action = func() {
...
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
if err := gSrv.Serve(lis); err != nil {
errCh <- errors.Wrap(err, "gRPC server")
}
}()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
select {
case err := <-errCh:
log.Println(err)
case <-sigCh:
log.Println("termination signal received. attempt graceful shutdown")
}
gSrv.GracefulStop()
wg.Wait()
log.Println("bye")
}
...
}
さて、せっかくサーバーも立ち上がったのでデバッグしてみましょう。
みなさんgRPCの動作確認には何を使っていますか?httpではないのでcurlは使えませんね。
おそらくはgRPC版のcurlである fullstorydev/grpcurl の人気が高いのではないかと思います。
このツールの使い方に関してはこちらの記事が詳しくてわかりやすいためオススメです。
明日はgRPCのGUIクライアントツールを紹介したいと思います。お楽しみに。