ノリでgRPCを勉強してみようと思ったら楽しかったので書きます。
gRPC is 何
公式ページはこちら。
ざっくりサマるとハイパフォーマンスなRPC(Remote Procedure Call)のこと。
異なるサービス間のやり取りを実装するプログラミング言語に縛られずにストリーミングでの情報伝達も可能にするRPCの一種です。
データフォーマットにProtocol Bufferを使いバイナリ化され、型付けされたデータを転送できる。
使いどころとしては例えば
- Microserviceのバックエンドサーバー間の通信
- モバイル向けがサービスの通信(通信量を節約)
- スピードを求められる通信
などがある。
またgRPCではHTTP/2を使うため、HTTP/1.1では実現できなかったサーバープッシュなどが可能である。
HTTP/1.1 | HTTP/2 |
---|---|
1リクエスト1レスポンス | ストリームで1TCP接続で複数のリクエストとレスポンスが可能 |
ヘッダーなどのオーバーヘッド | ヘッダーの圧縮/キャッシュで差分のみの送受信が可能 |
- | サーバープッシュが可能 |
Protocol Buffer
公式ページはこちら。
スキーマ言語で構造化されたデータをシリアライズするために使われる。
gRPCでのリクエストとレスポンスに用いるデータフォーマットで実装するプログラミング言語は色々使える。
メリットとしては
- 型安全
- バイナリ化されるため文字列で扱うよりも高速で通信可能
といった点があります。
JSONとの比較をすると下のような感じです。
JSON | Protocol Buffers |
---|---|
広く使われている | 少数 |
ほとんどのプログラミング言語で使える | 一部プログラミング言語のみ |
ネストが自由自在 | 複雑な構造には不向き |
人が読みやすい | バイナリ化された後では人間には読めない |
スキーマを強制できない | 型が保証される |
データサイズは大きい | 小さいデータサイズ |
利用の流れとしては
- スキーマ定義
- 各言語のオブジェクト生成
- バイナリにシリアライズする
という流れです。
Protocol Bufferファイル
まずはサンプルのファイルを見てみましょう。
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
syntax = "proto3";
はバージョンの宣言で特段理由がなければproto3を使いましょう。
messageは複数のフィールドを持つことができ、各フィールドは型情報、フィールド名、数値を設定します。
各スカラー型などはこちらを参照。
数値は1
から 536,870,911
を利用可能(ただし19,000
から 19,999
は予約されているため使用しない)で再利用できない様になっています。
Protocol Bufferはフィールドは名前ではなくタグ番号で管理するため一度採番したら変更できないことに注意しましょう。
ちなみに数値は1~15がよく使われるらしい(1byteでいけるため)。
Service
RCPの実装単位をServiceと言います。
1サービスに複数のメソッドを定義できる。
通信方式が4種ありそれぞれ
- Unary RPC
- Server Streaming RPC
- Client Streaming RPC
- Bidirectional Streaming RPC
です。
Unary RPC
クライアントから1リクエスト、サーバーから1レスポンスの通信方式
(REST APIでもお馴染み)
Server Streaming RPC
クライアントから1リクエスト、サーバーから複数レスポンスの通信方式
クライアントはサーバーからの送信完了まで通信を続ける。
プッシュ通知などに使える。
Client Streaming RPC
クライアントから複数リクエスト、サーバーから1レスポンスの通信方式
クライアントの終了を持ってレスポンスを返す。
大容量ファイルのアップロード時にチャンクするような時に使う。
Bidirectional Streaming RPC
クライアントから複数リクエスト、サーバーから複数レスポンスの通信方式
チャット、オンライン対戦ゲームで使うと良い。
いざ実践
ここまでgRPCとProtocol Bufferのあれこれを学んだので、実際に動かすまでを書いてみる。
Protocol Bufferのコードは以下。
Goで実装を行うことを前提とします。
syntax = "proto3";
package sample;
option go_package = "./pb";
message ListSampleRequest{};
message ListSampleResponse{
repeated string samplenames = 1;
};
message DownloadRequest {
string samplename = 1;
};
message DownloadResponse {
bytes data = 1;
};
message UploadRequest {
bytes data = 1;
};
message UploadResponse {
int32 size = 1;
};
message UploadAndNotifyProgressRequest {
bytes data = 1;
};
message UploadAndNotifyProgressResponse {
string message = 1;
};
service SampleService {
rpc ListSample (ListSampleRequest) returns (ListSampleResponse);
rpc Download(DownloadRequest) returns (stream DownloadResponse);
rpc Upload(stream UploadRequest) returns (UploadResponse);
rpc UploadAndNotifyProgress(stream UploadAndNotifyProgressRequest) returns (stream UploadAndNotifyProgressResponse);
}
いくつか補足
package
Protocol Bufferでは複数ファイルの分割が可能になっている。
他のファイルをimportするときにはパッケージ名.型名
とし、名前空間を定義できる。
option
メタデータをいれることができる。
option go_package = "./pb";
はGoで生成されるコードをどのパッケージに入れるかを設定します。
service
serviceにはRPCメソッドを定義します。
例えばrpc ListSample (ListSampleRequest) returns (ListSampleResponse);
ではUnary RPCのメソッドを定義しています。
リクエストの方を1つ目の()内に、レスポンスの方をreturnsに続く()に入れます。
またstreamにする場合は型の前にstream
を入れます。
これでProtocol BufferからGoのコードを生成します。
$ protoc -I. --go_out=. --go-grpc_out=. proto/*.proto
このコマンドを実行するとpb
ディレクトリの中にsample_grpc.pb.go
、sample.pb.go
の2ファイルが生成されます。
Goのコード
Goのバージョンは1.22としています。
(ストリーミング処理のいい例が思いつかなかったのでUnary RPCのみの実装例を示します。)
your-mod-nameはgo modのmodule名を示します。
まずはクライアント側の実装から。
package main
import (
"context"
"fmt"
"your-mod-name/pb"
"log"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.NewClient("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connext: %v", err)
}
defer conn.Close()
client := pb.NewSampleServiceClient(conn)
callListSample(client)
}
func callListSample(client pb.SampleServiceClient) {
res, err := client.ListSample(context.Background(), &pb.ListSampleRequest{})
if err != nil {
log.Fatalln(err)
}
fmt.Println(res)
}
特に難しいことはしておらずgrpc.NewClient("localhost:50051", grpc.WithInsecure())
でクライアントのインスタンスを生成して自動生成されたメソッドを呼び出しています。
grpc.WithInsecure()
は通信を暗号化せずに行うため本番環境で使う際には注意しましょう。
続いてサーバー側の実装。
package main
import (
"context"
"fmt"
"your-mod-name/pb"
"log"
"net"
"os"
"google.golang.org/grpc"
)
type server struct {
pb.UnimplementedSampleServiceServer
}
func (*server) ListSample(ctx context.Context, req *pb.ListSampleRequest) (*pb.ListSampleResponse, error) {
fmt.Println("ListeSample was invoked")
sampleNames := []string{"apple", "banana", "lemon"}
res := &pb.ListSampleResponse{
Samplenames: sampleNames,
}
return res, nil
}
func main() {
lis, err := net.Listen("tcp", "localhost:50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterSampleServiceServer(s, &server{})
fmt.Println("Server running")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
server/main.goを実行してclient/main.goを実行するとgRPCの通信を確認できます。