1週間(2024/09/22 ~ 2024/09/29):gRPCについて学習したのでそのレポート的なものを記述する。
参考文献
スターティングgRPC | インプレス NextPublishing
gRPCとは?
RPC (Remote Procedure Call)を実現するための実装の一つ。googleが開発した。
RPCとは
リモートの機能を呼び出すための技術。似たようなものにRESTなどが挙げられる。
RESTとの違い
- HTTP/2による高速通信
- バイナリによる通信のため通信帯域が少なめ
- ストリーミング通信
- インターセプタによるgRPCのメイン処理前後への処理埋め込み (認証や入力検証に用いる)
利用手順
protocol buffers スキーマファイルの作成 (RESTでいうところのopenapiと似たようなものという認識)
各サーバ、クライアントの実装
protocol buffersスキーマファイルをまず作成、これをコンパイルすることで各サーバ、クライアント用のライブラリが作成される。
実装
ひとまずシンプルにクライアント、サーバを作ってクライアントからサーバ側にある関数を呼び出すことができるか確認をする。
そのためのシナリオとして以下のようなものを検討。
サーバ側で検索用関数を実装、クライアント側からその関数を呼び出してデータベースを検索し結果を取得する。データベースには名前、年齢、メールアドレスが記載されているということにする。
データベースと記載したが本記事では文字列である名前をキーにして詳細な情報が入っているmap[string]Personという型の変数をデータの参照元とする。
Personは以下のような型 (goのPerson型作成はprotocが自動的に行うためわざわざこの型を用意する必要はない。)
type Person struct { Name string Age int32 Email string }
実装したサンプルは以下のリポジトリを参照のこと。
記事投稿当時のcommitへのリンク
https://github.com/ABC10946/grpc-learning/tree/d3111a1297c23c2f401f0837bd5401579f94d565
スキーマ作成
まずはスキーマを作成する。
SearchでSearchRequestを飛ばすとSearchResponseが返ってくるサービスを作成。
SearchRequestには文字列型query、SearchResponseにはプロフィール情報が入るPerson型と発見したか否かを示すbool型変数が定義される。
ここで注意したいことは、このprotobufファイルで記述したものは単なるインターフェイスに過ぎないというもので、このインターフェイスをやり取りするためのライブラリがprotocにより自動生成される。
実際に行いたい処理は人間が実装していく必要がある。(例えばここではSearch関数を実装する必要がある。)
syntax = "proto3"; option go_package = "gen/api"; package simple.maker; service SimpleSearchService { rpc Search(SearchRequest) returns (SearchResponse) {}; } message SearchRequest { string query = 1; } message SearchResponse { bool is_found = 1; Person person = 2; } message Person { string name = 1; int32 age = 2; string email = 3; }
コンパイル
protoc --proto_path=. --go_out=. proto/simple.proto --go-grpc_out=.
サーバの実装
重要な部分のみを抜粋したコードが以下。
api.RegisterSimpleSearchServiceServer(s, &server{})
でserverという構造体を指定しており、このserver構造体にSearch関数を実装している。
Register.*ServiceServerというものがprotocにより自動生成された関数であり、これの第二引数にはSearch関数がある構造体のinterfaceが指定されている。
このSearch関数こそが今回RPCされる対象の関数である。
このSearch関数自体もinterfaceのみ自動生成されており、これを満たすように内部を実装していく。
今回はSearchRequestを受け取ってそこからqueryを取得、そのquery文字列をキーにmap[string]Personから該当の人間のプロフィールを検索するというふうに実装する。
もちろんインターフェイス以外は、内部実装は自由にいじれるので他PostgreSQLなどとやりとりするORMを利用しても良い。
func (s *server) Search(ctx context.Context, req *api.SearchRequest) (*api.SearchResponse, error) { query := req.GetQuery() log.Printf("Query: %s", query) if data[query].Name != "" { person := data[query] return &api.SearchResponse{Person: &person, IsFound: true}, nil } return &api.SearchResponse{IsFound: false}, nil } func main() { port := 50051 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() api.RegisterSimpleSearchServiceServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
クライアントの実装
クライアント側ではgrpcのclientを初期化すればSearch関数を呼び出すことができる。
まるで別のライブラリを呼び出しているかのようにリモートサーバ上にある関数を呼び出すことができる。
func main() { flag.Parse() conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("failed to connect: %v", err) } defer conn.Close() c := api.NewSimpleSearchServiceClient(conn) r, err := c.Search(context.Background(), &api.SearchRequest{Query: *name}) if err != nil { log.Fatalf("failed to search: %v", err) } if r.GetIsFound() { log.Printf("Person found: %v", r.GetPerson()) return } else { log.Printf("Person not found") return } }
実行結果
実行してみるとjohnというユーザを検索してそのレスポンスが返ってくることが確認できる。
$ https://github.com/ABC10946/grpc-learning $ cd grpc-learning/simple $ cd server $ go run . -------別ターミナル $ cd grpc-learning/simple $ cd client $ go run . 2024/09/29 13:22:14 Person found: name:"john" age:20 email:"john@local" $ go run . -name jane 2024/09/29 13:22:18 Person found: name:"jane" age:20 email:"jane@local"