🦄

GoでgRPCを実装する

2021/07/05に公開約6,300字

はじめに

通信環境などがどんどん整備されてきて、gRPCを使ったサービスはどんどん増えてきています。

gRPCの利点を個人的に考えてみました。

  1. RPCの通信モデルを簡単に定義できる
  2. シンプルで理解しやすい
  3. メンテナンス性が高い
  4. 通信内容の変更に強い

また違う側面からみると、開発者にとっては極めて実装のみに注力できるフレームワークとも言えるかもしれません。
今回は、基本的なサーバとクライアントの作成をGo言語での実装を紹介してみたいと思います。

環境

実装イメージ

以下のようなサービスで実装を進めてみたいと思います。

大まかな流れ

gRPCを使用する場合、次の4つの大まかな手順で実装を進めます。

  1. プロシージャの定義を決める
  2. 使用するパラメータ値を設定する
  3. スタブ・スケルトンを生成する
  4. サーバとクライアントを実装する

準備

protocコマンドの導入

まず protocの導入を示します。コードを自動生成するにはコンピュターにprotocをインストールする必要があります。環境ごとにあわせて導入します。

MacOS

Homebrew でインストールします。

$ brew install protobuf
$ protoc --version
libprotoc 3.17.3

Ubuntu (Linux)

Windows

Go用のプラグイン

Go用のプロトコルコンパイラプラグインをインストール

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1

PATHを更新して、protocコンパイラがプラグインを見つけられるようにします

$ export PATH="$PATH:$(go env GOPATH)/bin"

プロシージャの定義を決める

プロシージャ定義とは、やり取りするデータの定義のことを指します。具体的には .proto ファイルに通信の定義を記述します。

ここでは、Personというサービスを作成します。

// Person サービス
service Person {....}

使用するパラメータ値を設定する

HelloRequestHelloResponse を使ってHelloというメッセージのやり取りを↓のように定義します。
パラメータはデータ型と番号だけで設定します。

// Person サービスの定義
service Person {
  // Unaryリクエスト
  rpc Hello (HelloRequest) returns (HelloResponse) {}
}

// リクエスト
message HelloRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

// レスポンス
message HelloResponse {
  string message = 1;
}

スタブ・スケルトンを生成する

データ定義された.proto ファイルをprotoc コマンドを使用してコードの自動生成を行います。その定義が反映されたサーバ用とクライアント用のスタブのコードが作成されます。
( github: protobuf-go )

protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    proto/person.proto

実際には↓のようにファイルが出力されます。

➜  grpc tree
.
└── proto
    ├── person.pb.go  <-自動生成された
    ├── person.proto
    └── person_grpc.pb.go  <-自動生成された

2つのファイルが生成されました。

  • person.pb.goHelloRequestHelloResponse のメッセージタイプの入力、シリアル化、および取得するためのコード。
  • person_grpc.pb.go: クライアントとサーバーコード。

サーバとクライアントを実装する

サーバの実装

自動生成されたファイル(person_grpc.pb.go)に下に示すようなサーバのインターフェイスが宣言されています。サーバ側の実装はこのインターフェイスを継承して進めます。

// 自動生成されたコードです。
// protoファイルで定義した内容がコードとして出力されています。
type PersonServer interface {
  Hello(context.Context, *HelloRequest) (*HelloResponse, error)
  mustEmbedUnimplementedPersonServer()
}

実際のサーバ側の実装コードを下に示します。

package main

import (
  "context"
  personpb "github/repository_name/grpc/proto"
  "log"
  "net"

  "google.golang.org/grpc"
)

const (
  port = ":50051"
)

type server struct {
  personpb.UnimplementedPersonServer
}

// Helloメソッドを実装することでprotoファイルで定義した内容と連携できます
func (*server) Hello(ctx context.Context, in *personpb.HelloRequest) (*personpb.HelloResponse, error) {
  // Getxxxメソッドも自動に作成されています。
  name := in.GetName()
  email := in.GetEmail()
  age := in.GetAge()
  result := fmt.Sprintf("Hello,%s (%d). email is %s", name, age, email)

  // 受け取ったメッセージを連結したレスポンスを返します。
  return &personpb.HelloResponse{
    Message: result,
  }, nil
}

func main() {
  // リッスンするportをを設定します
  lis, err := net.Listen("tcp", port)
  if err != nil {
    log.Fatalf("Failed to listen: %v", err)
  }

  var opts []grpc.ServerOption

  // サーバをインスタンス化します。
  s := grpc.NewServer(opts...)

  // RegisterXXXXメソッドも自動的に作成されています。
  personpb.RegisterPersonServer(s, &server{})

  // 起動確認様にログ出力をさせてみます
  log.Println("Waiting for gRPC request ....")
  log.Println("--------------")

  // サービスの起動を行います。
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
  }
}

動作確認してみます。

$ go run main.go
# このようにログが出力されていれば、起動成功です
2021/07/05 17:42:24 GRPC Server started !
2021/07/05 17:42:24 Server is waiting gRPC request ....
2021/07/05 17:42:24 --------------

クライアントの実装

クライアント側もGoで実装します。

自動生成されたperson_grpc.pb.go の中に、クライアントのインターフェイス部分を探します。
NewPersonClientという定義が見つかりました。

func NewPersonClient(cc grpc.ClientConnInterface) PersonClient {
  return &personClient{cc}
}

このインターフェイスを使ってクライアント側の実装を進めます。

ディレクトリは下に示す階層です。

➜  $ tree
.
├── client
│   └── main.go ←ここにクライアントの実装をします
├── proto
│   ├── person.pb.go
│   ├── person.proto
│   └── person_grpc.pb.go
└── server
    └── main.go
package main

import (
  "context"
  personpb "github/repository_name/grpc/proto"
  "log"

  "google.golang.org/grpc"
)

// Unaryリクエスト用のメソッドを定義します。
func postUnaryRequest(c personpb.PersonClient) {
  log.Println("Start Unary request")

  // Helloリクエストを作成します。
  // この定義はprotoファイルで定義しました。
  req := &personpb.HelloRequest{
    Name:  "Thomas Lathan",
    Age:   30,
    Email: "Withown@example.net",
  }

  // Helloリクエストを実行する
  res, err := c.Hello(context.Background(), req)
  if err != nil {
    log.Fatalf("error while calling Person request: %v", err)
  }
  log.Printf("Response from Hello Server: %v\n", res)
}

func main() {
  // オプション設定用のスライス
  var opts []grpc.DialOption
  // テストなのでfalse
  tls := false
  if tls {
    // セキュア通信処理を実装する
  } else {
    opts = append(opts, grpc.WithInsecure())
  }

  // サーバ側に接続をする。
  cc, err := grpc.Dial("localhost:50051", opts...)
  if err != nil {
    log.Fatalf("Could not connect: %v", err)
  }
  defer cc.Close()

  // クライアントをインスタンス化して
  c := personpb.NewPersonClient(cc)
  // リクエストを実行
  postUnaryRequest(c)
}

サーバ側とは別のターミナルからクライアントを実行してみます。

client go run main.go  
2021/07/05 20:30:58 Start Unary request
2021/07/05 20:30:58 Response from Hello Server: message:"Hello,Thomas Lathan (37). email is Withown@example.net"

サーバ側で処理された文字列が出力されたら成功です。
無事にgRPCで通信できました。

まとめ

今回はgRPCの基本的な実装を紹介してみました。想像していた時より実装のハードルが下がったのではないでしょうか。実際のサービスでは考慮する観点がもっとあるのでそのまま利用することはできません。

最初にgRPCの利点を挙げましたが、欠点もあります。

  1. クライアントとサーバの両方に特別なソフトウェアを導入しなければならない
  2. クライアントとサーバが別環境の場合、protoファイルの変更の追随を解決しなければならない
  3. gPRCで生成されたコードはクライアントとサーバのビルドプロセスに組み込まなければならない
  4. HTTP2通信ができる環境が必要になる

実際に使い始めるとまだまだあるかもしれません。gPRCにメリットが有るという場合には積極的に採用し、不安がある場合はOpenAPIなども選択肢とするなど、臨機応変に導入するのがいいと思います。

Discussion

ログインするとコメントできます