基礎チュートリアル
GoにおけるgRPCの基本チュートリアルです。
基礎チュートリアル
このチュートリアルでは、Goプログラマー向けにgRPCの基本的な使い方を紹介します。
この例に従うことで、以下の方法を学ぶことができます。
.proto
ファイルでサービスを定義する。- Protocol Bufferコンパイラを使用してサーバーとクライアントのコードを生成する。
- Go gRPC APIを使用して、サービス用のシンプルなクライアントとサーバーを作成する。
gRPCの概要を読み、Protocol Buffersに精通していることを前提としています。このチュートリアルの例では、proto3バージョンのProtocol Buffers言語を使用しています。詳しくは、proto3言語ガイドとGo生成コードガイドをご覧ください。
なぜgRPCを使うのか?
この例は、クライアントがルート上の地物に関する情報を取得し、ルートの概要を作成し、サーバーや他のクライアントと交通情報などのルート情報を交換できる、シンプルなルートマッピングアプリケーションです。
gRPCを使用すると、.proto
ファイルでサービスを一度定義し、gRPCがサポートする任意の言語でクライアントとサーバーを生成できます。生成されたクライアントとサーバーは、大規模データセンター内のサーバーから自分のタブレットまで、さまざまな環境で実行できます。異なる言語や環境間の通信の複雑さはすべてgRPCによって処理されます。また、効率的なシリアライズ、シンプルなIDL、簡単なインターフェース更新など、Protocol Buffersを使用するすべての利点も得られます。
セットアップ
クライアントとサーバーのインターフェースコードを生成するために必要なツールは既にインストールされているはずです。インストールされていない場合は、クイックスタートの前提条件セクションにあるセットアップ手順をご覧ください。
サンプルコードを入手する
サンプルコードは、grpc-goリポジトリの一部です。
リポジトリをzipファイルとしてダウンロードして解凍するか、リポジトリをクローンします
$ git clone -b v1.63.0 --depth 1 https://github.com/grpc/grpc-go
サンプルディレクトリに移動します
$ cd grpc-go/examples/route_guide
サービスの定義
最初の手順(gRPCの概要で説明したように)は、Protocol Buffersを使用してgRPC *サービス*とメソッドの*リクエスト*および*レスポンス*タイプを定義することです。完全な.proto
ファイルについては、routeguide/route_guide.protoを参照してください。
サービスを定義するには、.proto
ファイルで名前付き`service`を指定します
service RouteGuide {
...
}
次に、サービス定義内で`rpc`メソッドを定義し、それらのリクエストとレスポンスタイプを指定します。 gRPCでは、4種類のサービスメソッドを定義できます。これらはすべて`RouteGuide`サービスで使用されます。
*単純RPC*:クライアントがスタブを使用してサーバーにリクエストを送信し、通常の関数呼び出しのようにレスポンスが返ってくるのを待ちます。
// Obtains the feature at a given position. rpc GetFeature(Point) returns (Feature) {}
*サーバーサイドストリーミングRPC*:クライアントがサーバーにリクエストを送信し、一連のメッセージを読み取るためのストリームを取得します。クライアントは、メッセージがなくなるまで返されたストリームから読み取ります。この例でわかるように、*レスポンス*タイプの前に`stream`キーワードを配置することで、サーバーサイドストリーミングメソッドを指定します。
// Obtains the Features available within the given Rectangle. Results are // streamed rather than returned at once (e.g. in a response message with a // repeated field), as the rectangle may cover a large area and contain a // huge number of features. rpc ListFeatures(Rectangle) returns (stream Feature) {}
*クライアントサイドストリーミングRPC*:クライアントが一連のメッセージを書き込み、提供されたストリームを使用してサーバーに送信します。クライアントはメッセージの書き込みが完了すると、サーバーがすべてを読み取ってレスポンスを返すまで待ちます。*リクエスト*タイプの前に`stream`キーワードを配置することで、クライアントサイドストリーミングメソッドを指定します.
// Accepts a stream of Points on a route being traversed, returning a // RouteSummary when traversal is completed. rpc RecordRoute(stream Point) returns (RouteSummary) {}
*双方向ストリーミングRPC*:両側が読み書きストリームを使用して一連のメッセージを送信します。2つのストリームは独立して動作するため、クライアントとサーバーは好きな順序で読み書きできます。たとえば、サーバーはすべてのクライアントメッセージを受信するまで待ってからレスポンスを書き込むことも、メッセージを読み取ってからメッセージを書き込むことも、あるいは読み取りと書き込みの他の組み合わせを行うこともできます。各ストリームのメッセージの順序は保持されます。リクエストとレスポンスの両方の前に`stream`キーワードを配置することで、このタイプのメソッドを指定します。
// Accepts a stream of RouteNotes sent while a route is being traversed, // while receiving other RouteNotes (e.g. from other users). rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
.proto
ファイルには、サービスメソッドで使用されるすべてのリクエストおよびレスポンスタイプのProtocol Bufferメッセージタイプの定義も含まれています。たとえば、`Point`メッセージタイプは次のとおりです。
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
クライアントとサーバーのコード生成
次に、.protoサービス定義からgRPCクライアントとサーバーのインターフェースを生成する必要があります。これは、特別なgRPC Goプラグインを使用してProtocol Bufferコンパイラ`protoc`で行います。クイックスタートで行ったことと似ています。
`examples/route_guide`ディレクトリから、次のコマンドを実行します
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
routeguide/route_guide.proto
このコマンドを実行すると、routeguideディレクトリに次のファイルが生成されます
- `route_guide.pb.go`:リクエストおよびレスポンスメッセージタイプを設定、シリアライズ、および取得するためのすべてのProtocol Bufferコードが含まれています。
- `route_guide_grpc.pb.go`:以下が含まれています
- `RouteGuide`サービスで定義されたメソッドを使用してクライアントが呼び出すためのインターフェースタイプ(または*スタブ*)。
- サーバーが実装するためのインターフェースタイプ。これも`RouteGuide`サービスで定義されたメソッドを使用します。
サーバーの作成
最初に、`RouteGuide`サーバーを作成する方法を見てみましょう。 gRPCクライアントの作成にのみ関心がある場合は、このセクションをスキップして、クライアントの作成に直接進むことができます(ただし、いずれにしても興味深いと思うかもしれません!)。
`RouteGuide`サービスを機能させるには、2つの部分があります
- サービス定義から生成されたサービスインターフェースを実装する:サービスの実際の「作業」を行う。
- gRPCサーバーを実行して、クライアントからのリクエストをリッスンし、適切なサービス実装にディスパッチする。
サンプルの`RouteGuide`サーバーは、server/server.goにあります。その仕組みを詳しく見てみましょう。
RouteGuideの実装
ご覧のとおり、サーバーには、生成された`RouteGuideServer`インターフェースを実装する`routeGuideServer`構造体タイプがあります
type routeGuideServer struct {
...
}
...
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
...
}
...
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
...
}
...
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
...
}
...
単純RPC
`routeGuideServer`は、すべてのサービスメソッドを実装します。最初に最も単純なタイプである`GetFeature`を見てみましょう。これは、クライアントから`Point`を取得し、データベースから対応する地物情報を`Feature`で返します。
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
return feature, nil
}
}
// No feature was found, return an unnamed feature
return &pb.Feature{Location: point}, nil
}
メソッドには、RPCのコンテキストオブジェクトとクライアントの`Point` Protocol Bufferリクエストが渡されます。レスポンス情報を含む`Feature` Protocol Bufferオブジェクトと`error`を返します。メソッドでは、`Feature`に適切な情報を入力し、`nil`エラーとともに`return`して、RPCの処理が完了し、`Feature`をクライアントに返すことができることをgRPCに伝えます。
サーバーサイドストリーミングRPC
次に、ストリーミングRPCの1つを見てみましょう。 `ListFeatures`はサーバーサイドストリーミングRPCであるため、複数の`Feature`をクライアントに送り返す必要があります。
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) {
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
ご覧のとおり、メソッドパラメータで単純なリクエストオブジェクトとレスポンスオブジェクトを取得する代わりに、今回はリクエストオブジェクト(クライアントが`Feature`を見つけたい`Rectangle`)と、レスポンスを書き込むための特別な`RouteGuide_ListFeaturesServer`オブジェクトを取得します。
メソッドでは、返す必要のある数の`Feature`オブジェクトを入力し、`Send()`メソッドを使用して`RouteGuide_ListFeaturesServer`に書き込みます。最後に、単純なRPCと同様に、`nil`エラーを返して、レスポンスの書き込みが完了したことをgRPCに伝えます。この呼び出しでエラーが発生した場合、`nil`以外のエラーが返されます。 gRPCレイヤーは、それを適切なRPCステータスに変換して、ネットワーク経由で送信します。
クライアントサイドストリーミングRPC
では、もう少し複雑な例を見てみましょう。クライアント側ストリーミングメソッドの `RecordRoute` です。このメソッドでは、クライアントから `Point` のストリームを受け取り、その旅行情報を含む単一の `RouteSummary` を返します。ご覧のとおり、今回はメソッドにリクエストパラメータがありません。代わりに、サーバーがメッセージの読み書きに使用できる `RouteGuide_RecordRouteServer` ストリームを取得します。サーバーは `Recv()` メソッドを使用してクライアントメッセージを受信し、`SendAndClose()` メソッドを使用して単一のレスポンスを返します。
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
var pointCount, featureCount, distance int32
var lastPoint *pb.Point
startTime := time.Now()
for {
point, err := stream.Recv()
if err == io.EOF {
endTime := time.Now()
return stream.SendAndClose(&pb.RouteSummary{
PointCount: pointCount,
FeatureCount: featureCount,
Distance: distance,
ElapsedTime: int32(endTime.Sub(startTime).Seconds()),
})
}
if err != nil {
return err
}
pointCount++
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
featureCount++
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point)
}
lastPoint = point
}
}
メソッド本体では、`RouteGuide_RecordRouteServer` の `Recv()` メソッドを使用して、クライアントのリクエストをリクエストオブジェクト(この場合は `Point`)に繰り返し読み込みます。メッセージがなくなるまで、サーバーは各呼び出し後に `Recv()` から返されたエラーを確認する必要があります。エラーが `nil` の場合、ストリームは正常であり、読み取りを続行できます。`io.EOF` の場合、メッセージストリームは終了し、サーバーは `RouteSummary` を返すことができます。その他の値の場合は、エラーを「そのまま」返します。gRPC レイヤーによって RPC ステータスに変換されます。
双方向ストリーミングRPC
最後に、双方向ストリーミング RPC `RouteChat()` を見てみましょう。
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
... // look for notes to be sent to client
for _, note := range s.routeNotes[key] {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
今回は、クライアント側ストリーミングの例と同様に、メッセージの読み書きに使用できる `RouteGuide_RouteChatServer` ストリームを取得します。ただし、今回は、クライアントが *自身の* メッセージストリームにメッセージを書き込んでいる間に、メソッドのストリームを介して値を返します。
ここでの読み書きの構文は、クライアント側ストリーミングメソッドと非常によく似ています。ただし、サーバーは複数のレスポンスを書き込むため、ストリームの `SendAndClose()` メソッドではなく `Send()` メソッドを使用します。各側は常に相手のメッセージを受信した順序で取得しますが、クライアントとサーバーは任意の順序で読み書きできます。ストリームは完全に独立して動作します。
サーバーの起動
すべてのメソッドを実装したら、クライアントが実際にサービスを使用できるように gRPC サーバーを起動する必要があります。次のスニペットは、`RouteGuide` サービスに対してこれを実行する方法を示しています。
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
...
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)
サーバーをビルドして起動するには、次の手順を実行します。
- クライアントリクエストをリッスンするために使用するポートを `net.Listen(...)` を使用して指定します。
lis, err := net.Listen(...)
. - `grpc.NewServer(...)` を使用して gRPC サーバーのインスタンスを作成します。
- サービス実装を gRPC サーバーに登録します。
- ポートの詳細を指定してサーバーで `Serve()` を呼び出し、プロセスが強制終了されるか `Stop()` が呼び出されるまでブロッキング待機を実行します。
クライアントの作成
このセクションでは、`RouteGuide` サービスの Go クライアントを作成する方法について説明します。完全なクライアントコードの例は、grpc-go/examples/route_guide/client/client.go にあります。
スタブの作成
サービスメソッドを呼び出すには、まずサーバーと通信するための gRPC *チャネル* を作成する必要があります。これは、サーバーのアドレスとポート番号を `grpc.Dial()` に渡すことで作成します。
var opts []grpc.DialOption
...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
...
}
defer conn.Close()
サービスで認証資格情報(TLS、GCE 資格情報、JWT 資格情報など)が必要な場合は、`grpc.Dial` で `DialOptions` を使用して設定できます。`RouteGuide` サービスは資格情報を必要としません。
gRPC *チャネル* がセットアップされたら、RPC を実行するためのクライアント *スタブ* が必要です。これは、例の `.proto` ファイルから生成された `pb` パッケージによって提供される `NewRouteGuideClient` メソッドを使用して取得します。
client := pb.NewRouteGuideClient(conn)
サービスメソッドの呼び出し
次に、サービスメソッドの呼び出し方法を見てみましょう。gRPC-Go では、RPC はブロッキング/同期モードで動作することに注意してください。つまり、RPC 呼び出しはサーバーの応答を待機し、応答またはエラーを返します。
単純RPC
単純な RPC `GetFeature` の呼び出しは、ローカルメソッドの呼び出しとほぼ同じくらい簡単です。
feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
...
}
ご覧のとおり、以前に取得したスタブでメソッドを呼び出します。メソッドパラメータでは、リクエストプロトコルバッファオブジェクト(この場合は `Point`)を作成して設定します。また、必要に応じて RPC の動作を変更できる `context.Context` オブジェクトも渡します(たとえば、実行中の RPC のタイムアウト/キャンセル)。呼び出しがエラーを返さない場合、最初の戻り値からサーバーからの応答情報を読み取ることができます。
log.Println(feature)
サーバーサイドストリーミングRPC
ここでは、地理的な `Feature` のストリームを返すサーバー側ストリーミングメソッド `ListFeatures` を呼び出します。サーバーの作成 を既に読んでいる場合は、この一部は非常によく似ているように見えるかもしれません。ストリーミング RPC は両側で同様の方法で実装されます。
rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
...
}
for {
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
}
log.Println(feature)
}
単純な RPC と同様に、メソッドにコンテキストとリクエストを渡します。ただし、レスポンスオブジェクトを返す代わりに、`RouteGuide_ListFeaturesClient` のインスタンスを取得します。クライアントは `RouteGuide_ListFeaturesClient` ストリームを使用してサーバーのレスポンスを読み取ることができます。
`RouteGuide_ListFeaturesClient` の `Recv()` メソッドを使用して、サーバーのレスポンスをレスポンスプロトコルバッファオブジェクト(この場合は `Feature`)に繰り返し読み込みます。メッセージがなくなるまで、クライアントは各呼び出し後に `Recv()` から返されたエラー `err` を確認する必要があります。`nil` の場合、ストリームは正常であり、読み取りを続行できます。`io.EOF` の場合、メッセージストリームは終了しています。それ以外の場合は、`err` を介して渡される RPC エラーが発生している必要があります。
クライアントサイドストリーミングRPC
クライアント側ストリーミングメソッド `RecordRoute` はサーバー側メソッドに似ていますが、メソッドにコンテキストを渡すだけで、メッセージの書き込みと読み取りの両方で使用できる `RouteGuide_RecordRouteClient` ストリームが返される点が異なります。
// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
stream, err := client.RecordRoute(context.Background())
if err != nil {
log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
if err := stream.Send(point); err != nil {
log.Fatalf("%v.Send(%v) = %v", stream, point, err)
}
}
reply, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)
`RouteGuide_RecordRouteClient` には、サーバーにリクエストを送信するために使用できる `Send()` メソッドがあります。`Send()` を使用してクライアントのリクエストをストリームに書き込んだら、ストリームで `CloseAndRecv()` を呼び出して、書き込みが完了し、レスポンスを受信することを gRPC に知らせる必要があります。RPC ステータスは、`CloseAndRecv()` から返された `err` から取得します。ステータスが `nil` の場合、`CloseAndRecv()` からの最初の戻り値は有効なサーバーレスポンスになります。
双方向ストリーミングRPC
最後に、双方向ストリーミング RPC `RouteChat()` を見てみましょう。`RecordRoute` の場合と同様に、メソッドにコンテキストオブジェクトを渡すだけで、メッセージの書き込みと読み取りの両方で使用できるストリームが返されます。ただし、今回は、サーバーが *自身の* メッセージストリームにメッセージを書き込んでいる間に、メソッドのストリームを介して値を返します。
stream, err := client.RouteChat(context.Background())
waitc := make(chan struct{})
go func() {
for {
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("Failed to receive a note : %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
for _, note := range notes {
if err := stream.Send(note); err != nil {
log.Fatalf("Failed to send a note: %v", err)
}
}
stream.CloseSend()
<-waitc
ここでの読み書きの構文は、クライアント側ストリーミングメソッドと非常によく似ています。ただし、呼び出しが完了したら、ストリームの `CloseSend()` メソッドを使用します。各側は常に相手のメッセージを受信した順序で取得しますが、クライアントとサーバーは任意の順序で読み書きできます。ストリームは完全に独立して動作します。
試してみよう!
`examples/route_guide` ディレクトリから次のコマンドを実行します。
サーバーを実行します。
$ go run server/server.go
別のターミナルから、クライアントを実行します。
$ go run client/client.go
次のような出力が表示されます。
Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 >
name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 >
...
name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 >
Traversing 56 points.
Route summary: point_count:56 distance:497013163
Got message First message at point(0, 1)
Got message Second message at point(0, 2)
Got message Third message at point(0, 3)
Got message First message at point(0, 1)
Got message Fourth message at point(0, 1)
Got message Second message at point(0, 2)
Got message Fifth message at point(0, 2)
Got message Third message at point(0, 3)
Got message Sixth message at point(0, 3)