この記事は2019/2/14時点のgRPC Basic - Goを和訳したものです。
このチュートリアルでは、gRPCを利用するGoプログラマー向けの基本的な手引きを提供します。
この例を見ていくことで、以下の方法を学ぶことができます。
- .protoファイル内のサービスの定義
- プロトコルバッファコンパイラによるクライアント・サーバーコードの生成
- サービス用のシンプルなクライアント・サーバーを書く為のGoのgRPC APIの利用
このチュートリアルは、あなたがOverviewを読み、protocol buffersに精通していると仮定します。このチュートリアルの例ではproto3バージョンのプロトコルバッファを使用していることに注意してください:proto3 language guideとGo generated code guideでもっと調べることができます。
なぜgRPCを使うのか?
この例では、クライアントがルート上の特徴に関する情報を取得し、ルートのサマリーを作成し、トラフィックの更新などのルート情報をサーバーや他のクライアントと交換できるようにする簡単なルートマッピングアプリケーションを紹介します。
gRPCを利用すると、.protoファイルにサービスを一度定義して、gRPCがサポートする任意の言語でクライアントとサーバーを実装できます。これは、Google内のサーバーから自分のタブレットまで、さまざまな環境で実行できますし、異なる言語と環境の間の複雑なコミュニケーションはすべてgRPCによって処理されます。 また、効率的なシリアル化、単純なIDL、および簡単なインターフェイスの更新など、プロトコルバッファを扱うことによる全ての利点も得られます。
サンプルコードとセットアップ
このチュートリアルのサンプルコードは、grpc/grpc-go/examples/route_guideにあります。例をダウンロードするには、次のコマンドを実行してgrpc-goリポジトリを複製します。
$ go get google.golang.org/grpc
それから現在のディレクトリをgrpc-go/examples/route_guide
に変更します。
$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide
サーバーとクライアントのインターフェイスコードを生成するための関連ツールもインストールする必要があります。まだ行っていない場合は、Go Quick Startの設定手順に従ってください。
サービスの定義
はじめのステップは(Overviewからわかるように)gRPCサービス、メソッド、protocol buffersを使用したリクエストとレスポンスの型を定義することです。
examples/route_guide/routeguide/route_guide.protoに完全な.protoファイルがあります。
サービスを定義するには、.protoファイルに名前付きサービスを指定します。
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ファイルには、サービスメソッドで使用されるすべてのリクエストとレスポンスの型に対するプロトコルバッファのメッセージ型定義も含まれています。たとえば、次のような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プラグインと共にプロトコルバッファコンパイラ protoc
を使って行います。これはquickstart guideで行ったことと似ています。
route_guideのサンプルディレクトリから、次のコマンドを実行します。
protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide
このコマンドを実行すると、route_guideサンプルディレクトリの下のrouteguideディレクトリに次のファイルが生成されます。
- route_guide.pb.go
このファイルは以下を含みます。
- リクエストとレスポンスのメッセージ型を生成、シリアル化、および取得するための全てのプロトコルバッファコード
- クライアントがRouteGuideサービスで定義されたメソッドを使用して呼び出すためのインターフェース型(またはスタブ)。
- RouteGuideサービスで定義されているメソッドを使用して、サーバーが実装するためのインターフェース型
サーバーの作成
まずRouteGuideサーバーを作成する方法を見てみましょう。 gRPCクライアントの作成だけに興味がある場合は、このセクションを飛ばしてクライアントの作成に直接進むことができます(とにかく面白いかもしれませんが)。
RouteGuideサービスに仕事をさせるために2つのパートがあります。
-
私たちのサービス定義から生成されたサービスインターフェースを実装する、サービスの実際の「仕事」をする部分
-
gRPCサーバーを実行して、クライアントからのリクエストを待ち受け、それらを正しいサービス実装に割り当てる部分
サンプルのRouteGuideサーバーはgrpc-go/examples/route_guide/server/server.goにあります。 それがどのように機能するかを詳しく見てみましょう。
RouteGuideの実装
ご覧のとおり、このサーバーには、生成されたRouteGuideServerインターフェースを実装するrouteGuideServerのstruct型があります。
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
を取得し、それに対応する特徴情報をデータベースから返します。
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{"", point}, nil
}
このメソッドには、RPC用のコンテキストオブジェクトとクライアントのPoint
プロトコルバッファリクエストが渡されます。 これはレスポンス情報とエラーを含むFeature
プロトコルバッファオブジェクトを返します。 このメソッドでは、適切な情報でFeature
を生成し、それnil
エラーと共に返して、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
を見つけたい矩形)とレスポンスを書き込むための特別なRouteGuide_ListFeaturesServer
オブジェクトを取得します。
t
このメソッドでは、返す必要があるだけ多くのFeature
オブジェクトを生成し、Send()
メソッドを使用してそれらをRouteGuide_ListFeaturesServer
に書き込みます。 最後に、単純なRPCのように、応答を書き終えたことをgRPCに伝えるためにnil
エラーを返します。 この呼び出しでエラーが発生した場合は、nil
以外のエラーが返されます。 gRPCレイヤはそれを適切なRPCステータスに変換してワイヤ上に送信します。
クライアントサイドストリーミングRPC
もう少し複雑な点を見てみましょう。クライアントサイドのストリーミングメソッドRecordRoute
では、クライアントからPoint
のストリームを取得し、そのトリップに関する情報を含む1つの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
)に繰り返し読み込みます。呼び出しのたびに、サーバーはRead()
から返されたエラーを確認する必要があります。これが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
サービスでこれを行う方法を示しています。
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
... // determine whether to use TLS
grpcServer.Serve(lis)
サーバーを構築して起動するには、次のようにします。
-
lis、err:= net.Listen( "tcp"、fmt.Sprintf( ":%d"、* port))
を使用して、クライアント要求を待機するために使用するポートを指定します。 -
grpc.NewServer()
を使用してgRPCサーバーのインスタンスを作成します。 - gRPCサーバーにサービス実装を登録してください。
- プロセスが強制終了されるか
Stop()
が呼び出されるまでブロッキング待機をするためにポートの詳細でサーバー上でServe()
を呼び出します。
クライアントの作成
このセクションでは、RouteGuide
サービス用のGoクライアントの作成について説明します。 grpc-go/examples/route_guide/client/client.goに、完全なサンプルクライアントコードがあります。
スタブの作成
サービスメソッドを呼び出すには、まずサーバーと通信するためのgRPCチャネルを作成する必要があります。次のようにサーバーアドレスとポート番号をgrpc.Dial()
に渡すことで作成します。
conn, err := grpc.Dial(*serverAddr)
if err != nil {
...
}
defer conn.Close()
あなたがリクエストを行うサービスが必要とするなら、grpc.Dial
でauth資格情報(例えばTLS、GCE資格情報、JWT資格情報)を設定するために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
)を作成して移植します。 また、context.Context
オブジェクトを渡します。これにより、必要に応じてRPCの動作(タイムアウトまたは処理中のRPCのキャンセルなど)を変更できます。 呼び出しがエラーを返さない場合は、最初の戻り値からサーバーからのレスポンスを読み取ることができます。
log.Println(feature)
サーバーサイドストリーミングRPC
ここでは、サーバーサイドのストリーミングメソッド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
の場合、メッセージストリームは終了しています。 そうでなければ、RPCエラーが存在しなければならず、それはerr
を通して渡されます。
クライアントサイドストリーミング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に書き込みが完了したことを通知し、応答を受け取ることを期待している必要があります。 CloseAndRecv()
から返されたerr
からRPCのステータスを取得します。 ステータスが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()
メソッドを使用する点を除いて、クライアント側のストリーミングメソッドと非常によく似ています。 どちらの側も、書き込まれた順に相手のメッセージを常に受け取りますが、クライアントとサーバーの両方が任意の順序で読み書きできます。ストリームは完全に独立して動作します。
やってみよう!
サーバをコンパイルして実行するには、あなたがフォルダ$GOPATH/src/google.golang.org/grpc/examples/route_guide
にいると仮定します。
$ go run server/server.go
同様に、クライアントを実行するには次のようにします
$ go run client/client.go