基本チュートリアル

Go における gRPC の基本的なチュートリアル入門。

基本チュートリアル

Go における gRPC の基本的なチュートリアル入門。

このチュートリアルは、gRPC の操作について Go プログラマー向けの基本的な入門情報を提供します。

この例を段階的に実行することで、以下のことを学びます。

  • .protoファイルでサービスを定義する。
  • プロトコルバッファコンパイラを使用して、サーバーとクライアントのコードを生成する。
  • Go gRPC API を使用して、サービス用のシンプルなクライアントとサーバーを記述します。

gRPC の はじめに を読み、 protocol buffers に慣れていることを前提とします。このチュートリアルの例では protocol buffers 言語の proto3 バージョンを使用しています。詳細については、 proto3 言語ガイド および Go 生成コードガイド を参照してください。

gRPCを使用する理由

私たちの例は、クライアントがルート上の機能に関する情報を取得したり、ルートの要約を作成したり、サーバーや他のクライアントと交通情報などのルート情報を交換したりできる、シンプルなルートマッピングアプリケーションです。

gRPCを使用すると、1つの.protoファイルでサービスを一度定義し、gRPCがサポートする任意の言語でクライアントとサーバーを生成できます。これにより、大規模データセンター内のサーバーから個人のタブレットまで、さまざまな環境で実行できます。異なる言語や環境間の通信の複雑さはすべてgRPCによって処理されます。また、効率的なシリアライゼーション、シンプルなIDL、簡単なインターフェース更新など、プロトコルバッファを使用する利点もすべて得られます。

セットアップ

クライアントとサーバーのインターフェイスコードを生成するために必要なツールはすでにインストールされているはずです。まだの場合は、セットアップ手順について クイックスタート前提条件 セクションを参照してください。

サンプルコードを取得する

サンプルコードは grpc-go リポジトリの一部です。

  1. リポジトリを zip ファイルとしてダウンロード して解凍するか、リポジトリをクローンしてください。

    git clone -b v1.74.2 --depth 1 https://github.com/grpc/grpc-go
    
  2. サンプルディレクトリに移動します。

    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ファイルには、サービスメソッドで使用されるすべてリクエストとレスポンスの型に対応するプロトコルバッファメッセージ型定義も含まれています。たとえば、ここに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 して、gRPC が RPC の処理を完了したこと、および Feature をクライアントに返すことができることを伝えます。

サーバーサイドストリーミング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_RecordRouteServerRecv() メソッドを使用して、リクエストオブジェクト(この場合は Point)にクライアントのリクエストを繰り返し読み込みます。メッセージがなくなるまで続きます。サーバーは、各呼び出し後に Recv() から返されるエラーをチェックする必要があります。これが nil の場合、ストリームはまだ有効で、読み込みを続けることができます。io.EOF の場合、メッセージストリームは終了し、サーバーは RouteSummary を返すことができます。それ以外の値の場合は、エラーを「そのまま」返します。これにより、gRPC レイヤーによって RPC ステータスに変換されます。

双方向ストリーミング RPC

最後に、双方向ストリーミングRPCRouteChat()を見てみましょう。

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)

サーバーをビルドして開始するには、

  1. クライアントリクエストをリッスンしたいポートを次のように指定します。
    lis, err := net.Listen(...).
  2. grpc.NewServer(...) を使用して gRPC サーバーのインスタンスを作成します。
  3. サービス実装を gRPC サーバーに登録します。
  4. サーバーの Serve() をポート詳細とともに呼び出して、プロセスが終了するか Stop() が呼び出されるまでブロックして待機します。

クライアントを作成する

このセクションでは、RouteGuide サービス用の Go クライアントの作成について説明します。完全なクライアントコード例は、grpc-go/examples/route_guide/client/client.go で確認できます。

スタブを作成する

サービスメソッドを呼び出すには、まずサーバーと通信するための gRPC *チャネル* を作成する必要があります。これは、次のようにサーバーのアドレスとポート番号を grpc.NewClient() に渡すことで作成します。

var opts []grpc.DialOption
...
conn, err := grpc.NewClient(*serverAddr, opts...)
if err != nil {
  ...
}
defer conn.Close()

サービスが認証資格情報を必要とする場合、grpc.NewClientDialOptions を使用して認証資格情報(例: TLS、GCE 認証情報、または JWT 認証情報)を設定できます。RouteGuide サービスは認証資格情報を必要としません。

gRPC *チャネル* がセットアップされたら、RPC を実行するためのクライアント *スタブ* が必要です。これは、例の .proto ファイルから生成された pb パッケージによって提供される NewRouteGuideClient メソッドを使用して取得します。

client := pb.NewRouteGuideClient(conn)

サービスメソッドを呼び出す

次に、サービスメソッドの呼び出し方法を見てみましょう。gRPC-Go では、RPC はブロック/同期モードで動作することに注意してください。これは、RPC 呼び出しがサーバーからの応答を待機し、応答またはエラーを返すことを意味します。

シンプルなRPC

シンプルなRPCGetFeatureの呼び出しは、ローカルメソッドの呼び出しとほとんど同じくらい簡単です。

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
  ...
}

ご覧のとおり、以前に取得したスタブでメソッドを呼び出します。メソッドパラメータでは、リクエスト protocol buffer オブジェクト(この場合は Point)を作成してポピュレートします。また、context.Context オブジェクトを渡します。これにより、RPC の動作(例: 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_ListFeaturesClientRecv() メソッドを使用して、サーバーのレスポンスをレスポンス protocol buffer オブジェクト(この場合は 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() メソッドを使用します。各側は常に、書き込まれた順序で相手のメッセージを取得しますが、クライアントとサーバーはどちらも任意の順序で読み書きできます。ストリームは完全に独立して動作します。

試してみましょう!

examples/route_guide ディレクトリから次のコマンドを実行します。

  1. サーバーを実行する

    go run server/server.go
    
  2. 別のターミナルで、クライアントを実行する

    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)