gRPC学習レポート new-learning-1

1週間(2024/09/22 ~ 2024/09/29):gRPCについて学習したのでそのレポート的なものを記述する。

参考文献

スターティングgRPC | インプレス NextPublishing

gRPC

gRPCとは?

RPC (Remote Procedure Call)を実現するための実装の一つ。googleが開発した。

RPCとは

リモートの機能を呼び出すための技術。似たようなものにRESTなどが挙げられる。

RESTとの違い

  • HTTP/2による高速通信
  • バイナリによる通信のため通信帯域が少なめ
  • ストリーミング通信
  • インターセプタによるgRPCのメイン処理前後への処理埋め込み (認証や入力検証に用いる)

利用手順

  1. protocol buffers スキーマファイルの作成 (RESTでいうところのopenapiと似たようなものという認識)

  2. 各サーバ、クライアントの実装

protocol buffersスキーマファイルをまず作成、これをコンパイルすることで各サーバ、クライアント用のライブラリが作成される。

実装

ひとまずシンプルにクライアント、サーバを作ってクライアントからサーバ側にある関数を呼び出すことができるか確認をする。

そのためのシナリオとして以下のようなものを検討。

サーバ側で検索用関数を実装、クライアント側からその関数を呼び出してデータベースを検索し結果を取得する。データベースには名前、年齢、メールアドレスが記載されているということにする。

データベースと記載したが本記事では文字列である名前をキーにして詳細な情報が入っているmap[string]Personという型の変数をデータの参照元とする。

Personは以下のような型 (goのPerson型作成はprotocが自動的に行うためわざわざこの型を用意する必要はない。)

type Person struct {
    Name string
    Age int32
    Email string
}

実装したサンプルは以下のリポジトリを参照のこと。

github.com

記事投稿当時の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"