grpcコースをやってみる〜その3〜
今回何をやるか
hatehate-nazenaze.hatenablog.com
前回はデータスキーマとそれを利用したapiを定義したが、今回はそれを動作させるサーバを定義する
serverの雛形を作る
- api/sever.goのコードの中身をコピペする
- 異なるのは
http
リクエストではなく、grpc
リクエストを取り扱う点 - フィールドである
config
、store
、tokenMaker
は引き続き必要である - 以下は削除する
- ginで動作するValidatorはgrpcでは動作しないため削除
- routerのセットアップもgrpcにはルートはないため削除
- クライアントはrpcを利用してあたかもローカルの関数を呼び出すかのように処理を呼び出す
// gapi/server.go type Server struct { config util.Config store db.Store tokenMaker token.Maker } func NewServer(config util.Config, store db.Store) (*Server, error) { tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey) if err != nil { return nil, fmt.Errorf("cannot create token maker: %w", err) } server := &Server{ config: config, store: store, tokenMaker: tokenMaker, } return server, nil }
serverが必要なインターフェースを継承する様に修正する
grpcサーバとして振る舞うには、自動生成された以下のインターフェースを継承する必要がある
// pb/service_simple_bank_grpc.pb.go type SimpleBankServer interface { CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) LoginUser(context.Context, *LoginUserRequest) (*LoginUserResponse, error) mustEmbedUnimplementedSimpleBankServer() }
以下のようにmustEmbedUnimplementedSimpleBankServer
メソッドを持っているUnimplementedSimpleBankServer
をフィールドに追加する
type Server struct { // 追加 pb.UnimplementedSimpleBankServer config util.Config store db.Store tokenMaker token.Maker }
mustEmbedUnimplementedSimpleBankServer()とは?
- UnimplementedSimpleBankServerをフィールドとして持つことにより、サーバは未実装のメソッドを持つことができる
- IFだけを定義した状態で徐々に実装していくことが可能、チーム開発などで便利そう
- 定義内容は以下の通り
// pb/service_simple_bank_grpc.pb.go // UnimplementedSimpleBankServer must be embedded to have forward compatible implementations. type UnimplementedSimpleBankServer struct { } func (UnimplementedSimpleBankServer) CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") } func (UnimplementedSimpleBankServer) LoginUser(context.Context, *LoginUserRequest) (*LoginUserResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LoginUser not implemented") } func (UnimplementedSimpleBankServer) mustEmbedUnimplementedSimpleBankServer() {}
serverの呼び出し元であるmain.goファイルを修正する
現状以下のようにginでhttpサーバを構築している
// main/main.go func main() { config, err := util.LoadConfig(".") if err != nil { log.Fatal("cannot load config:", err) } conn, err := sql.Open(config.DBDriver, config.DBSource) if err != nil { log.Fatal("cannot connect to db:", err) } store := db.NewStore(conn) server, err := api.NewServer(config, store) if err != nil { log.Fatal("cannot create server:", err) } err = server.Start(config.ServerAddress) if err != nil { log.Fatal("cannot start server:", err) } }
これを以下の様にすることで、grpcとhttpを切り替えやすくする ※動画の都合上、httpサーバも残しておきたいとのこと
// main/main.go func main() { config, err := util.LoadConfig(".") if err != nil { log.Fatal("cannot load config:", err) } conn, err := sql.Open(config.DBDriver, config.DBSource) if err != nil { log.Fatal("cannot connect to db:", err) } store := db.NewStore(conn) runGinServer(config, store) } // ここに今回の処理を追記していく func runGrpcServer(config util.Config, store db.Store) { } func runGinServer(config util.Config, store db.Store) { server, err := api.NewServer(config, store) if err != nil { log.Fatal("cannot create server:", err) } err = server.Start(config.ServerAddress) if err != nil { log.Fatal("cannot start server:", err) } }
main/main.go runGrpcServerメソッドの中身を実装する
今回作成したサーバをgrpcサーバに対して登録する
// main/main.go func runGrpcServer(config util.Config, store db.Store) { server, err := gapi.NewServer(config, store) if err != nil { log.Fatal("cannot create server:", err) } grpcServer := grpc.NewServer() pb.RegisterSimpleBankServer(grpcServer, server) }
grpc.NewServer()
サービス未登録で、リクエストを受け付けていないサーバを作成する
メソッドのコメントは以下のような記載になっている
// grpc@v1.43.0/server.go // NewServer creates a gRPC server which has no service registered and has not // started to accept requests yet.
RegisterSimpleBankServer()
引数で受け取ったgrpcサーバに対して、サービスの説明とサービスを登録
// pb/service_simple_bank_grpc.pb.go func RegisterSimpleBankServer(s grpc.ServiceRegistrar, srv SimpleBankServer) { s.RegisterService(&SimpleBank_ServiceDesc, srv)}
s.RegisterService()
// ServiceRegistrar wraps a single method that supports service registration. It // enables users to pass concrete types other than grpc.Server to the service // registration methods exported by the IDL generated code. type ServiceRegistrar interface { // RegisterService registers a service and its implementation to the // concrete type implementing this interface. It may not be called // once the server has started serving. // desc describes the service and its methods and handlers. impl is the // service implementation which is passed to the method handlers. RegisterService(desc *ServiceDesc, impl interface{}) }
SimpleBank_ServiceDesc
サービスに関する説明のよう
// pb/service_simple_bank_grpc.pb.go // SimpleBank_ServiceDesc is the grpc.ServiceDesc for SimpleBank service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var SimpleBank_ServiceDesc = grpc.ServiceDesc{ ServiceName: "pb.SimpleBank", HandlerType: (*SimpleBankServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "CreateUser", Handler: _SimpleBank_CreateUser_Handler, }, { MethodName: "LoginUser", Handler: _SimpleBank_LoginUser_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "service_simple_bank.proto", }
reflection.Register(grpcServer)を追記する
この一文を追加することで、クライアントが容易にサービスの一覧や、型や、詳細を取得する事が出来る
grpc-goのリフレクションについて調べてみた - Qiita
// main/main.go func runGrpcServer(config util.Config, store db.Store) { server, err := gapi.NewServer(config, store) if err != nil { log.Fatal("cannot create server:", err) } grpcServer := grpc.NewServer() pb.RegisterSimpleBankServer(grpcServer, server) // 追記 reflection.Register(grpcServer) }
設定を編集する
grpcサーバのポートを追加する
環境定義ファイルにgrpcサーバのポートを追加する
DB_DRIVER=postgres DB_SOURCE=postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable HTTP_SEVER_ADDRESS=0.0.0.0:8080 GRPC_SEVER_ADDRESS=0.0.0.0:9090 TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012 ACCESS_TOKEN_DURATION=15m
合わせてマッピング先のデータ構造を修正する
// config.go type Config struct { DBDriver string `mapstructure:"DB_DRIVER"` DBSource string `mapstructure:"DB_SOURCE"` HttpServerAddress string `mapstructure:"HTTP_SEVER_ADDRESS"` GrpcServerAddress string `mapstructure:"GRPC_SEVER_ADDRESS"` TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"` }
リクエスト受付処理を追記する
// main.go func runGrpcServer(config util.Config, store db.Store) { server, err := gapi.NewServer(config, store) if err != nil { log.Fatal("cannot create server:", err) } grpcServer := grpc.NewServer() pb.RegisterSimpleBankServer(grpcServer, server) reflection.Register(grpcServer) // 追記 listener, err := net.Listen("tcp", config.GrpcServerAddress) if err != nil { log.Fatal("cannot create listener", err) } log.Printf("start gRPC server at %s", listener.Addr().String()) err = grpcServer.Serve(listener) if err != nil { log.Fatal("cannot start gRPC server") } }
listener, err := net.Listen("tcp", config.GrpcServerAddress)
コネクションを取得することらしい ネットワーク|Goから学ぶI/O
grpcServer.Serve(listener)
サービスを登録した状態のgrpcServer
に対して、コネクションを渡してリクエストの受付を開始すると理解した
// Serve accepts incoming connections on the listener lis, creating a new // ServerTransport and service goroutine for each. The service goroutines // read gRPC requests and then call the registered handlers to reply to them. // Serve returns when lis.Accept fails with fatal errors. lis will be closed when // this method returns. // Serve will return a non-nil error unless Stop or GracefulStop is called. func (s *Server) Serve(lis net.Listener) error {
実際に動作させてみる
// main.go ffunc main() { config, err := util.LoadConfig(".") if err != nil { log.Fatal("cannot load config:", err) } conn, err := sql.Open(config.DBDriver, config.DBSource) if err != nil { log.Fatal("cannot connect to db:", err) } store := db.NewStore(conn) // grpcサーバ起動へ変更 runGrpcServer(config, store) }
起動
以下の通り起動自体は成功する
~/develop/simple-bank-cp | master +2 !4 make server INT | 4s go run main.go 2023/04/28 23:32:56 start gRPC server at [::]:9090
動作確認
Evansというgrpc clientを利用する
evans -r repl
If your server is enabling gRPC reflection, you can launch Evans with only -r (--reflection) option. gRPC reflectionが有効になっている場合、-rオプションをつけることで起動できるらしい そのままやるとgRPCのデフォルトポートである50051に接続しにいくため、エラーとなる 以下の通りオプションを追加することで接続に成功する
evans --host localhost --port 9090 -r repl
~/develop/simple-bank-cp | master +2 !4 evans --host localhost --port 9090 -r repl 1 err ______ | ____| | |__ __ __ __ _ _ __ ___ | __| \ \ / / / _. | | '_ \ / __| | |____ \ V / | (_| | | | | | \__ \ |______| \_/ \__,_| |_| |_| |___/ more expressive universal gRPC client pb.SimpleBank@localhost:9090>
繋いだ状態でいくつかリクエストを試す事が可能。 ※まだ未実装のため、エラーとなる