GoでgRPCを実装する
はじめに
通信環境などがどんどん整備されてきて、gRPCを使ったサービスはどんどん増えてきています。
gRPCの利点を個人的に考えてみました。
- RPCの通信モデルを簡単に定義できる
- シンプルで理解しやすい
- メンテナンス性が高い
- 通信内容の変更に強い
また違う側面からみると、開発者にとっては極めて実装のみに注力できるフレームワークとも言えるかもしれません。
今回は、基本的なサーバとクライアントの作成をGo言語での実装を紹介してみたいと思います。
環境
- Mac OS 11.2
- Go 1.16
- VSCode
実装イメージ
以下のようなサービスで実装を進めてみたいと思います。
大まかな流れ
gRPCを使用する場合、次の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 {....}
使用するパラメータ値を設定する
HelloRequest
と HelloResponse
を使って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.go
:HelloRequest
、HelloResponse
のメッセージタイプの入力、シリアル化、および取得するためのコード。 -
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の利点を挙げましたが、欠点もあります。
- クライアントとサーバの両方に特別なソフトウェアを導入しなければならない
- クライアントとサーバが別環境の場合、
proto
ファイルの変更の追随を解決しなければならない - gPRCで生成されたコードはクライアントとサーバのビルドプロセスに組み込まなければならない
- HTTP2通信ができる環境が必要になる
実際に使い始めるとまだまだあるかもしれません。gPRCにメリットが有るという場合には積極的に採用し、不安がある場合はOpenAPIなども選択肢とするなど、臨機応変に導入するのがいいと思います。
Discussion