gotoshin

主に学んだ事の自分メモ用です。記事に書くまでも無いような事はhttps://scrapbox.io/study-diary/に書いてます。

grpcコースをやってみる〜その3〜

www.youtube.com

今回何をやるか

hatehate-nazenaze.hatenablog.com

前回はデータスキーマとそれを利用したapiを定義したが、今回はそれを動作させるサーバを定義する

serverの雛形を作る

  • api/sever.goのコードの中身をコピペする
  • 異なるのはhttpリクエストではなく、grpcリクエストを取り扱う点
  • フィールドであるconfigstoretokenMakerは引き続き必要である
  • 以下は削除する
    • 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を利用する

github.com

brew tapとは - Qiita

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>

繋いだ状態でいくつかリクエストを試す事が可能。 ※まだ未実装のため、エラーとなる