1. はじめに
この Codelab では、gRPC-Go を使用して、Go で記述されたルート マッピング アプリケーションの基盤となるクライアントとサーバーを作成します。
このチュートリアルを完了すると、gRPC を使用してリモート サーバーに接続し、地図上の特定の座標にあるものの名前または郵便番号を取得するクライアントが作成されます。本格的なアプリケーションでは、このクライアント サーバー設計を使用して、ルート上のスポットを列挙または要約できます。
サービスは Protocol Buffers ファイルで定義されます。このファイルは、クライアントとサーバーが相互に通信できるように、クライアントとサーバーのボイラープレート コードを生成するために使用されます。これにより、この機能を実装する時間と労力を節約できます。
この生成されたコードは、サーバーとクライアント間の通信の複雑さだけでなく、データのシリアル化と逆シリアル化も処理します。
学習内容
- プロトコル バッファを使用してサービス API を定義する方法。
- 自動コード生成を使用して、プロトコル バッファ定義から gRPC ベースのクライアントとサーバーを構築する方法。
- gRPC を使用したクライアント サーバー通信の理解。
この Codelab は、gRPC を初めて使用する Go デベロッパー、gRPC の復習を希望するデベロッパー、分散システムの構築に関心のある方を対象としています。gRPC の経験は必要ありません。
2. 始める前に
前提条件
次のものがインストールされていることを確認します。
- Go ツールチェーン バージョン 1.24.5 以降。インストール手順については、Go のスタートガイドをご覧ください。
- プロトコル バッファ コンパイラ
protocバージョン 3.27.1 以降。インストール手順については、コンパイラのインストール ガイドをご覧ください。 - Go と gRPC のプロトコル バッファ コンパイラ プラグイン。これらのプラグインをインストールするには、次のコマンドを実行します。
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
プロトコル バッファ コンパイラがプラグインを見つけられるように、PATH 変数を更新します。
export PATH="$PATH:$(go env GOPATH)/bin"
コードを取得する
この Codelab では、完全にゼロから始める必要がないように、アプリケーションのソースコードのスケルトンが用意されています。次の手順では、プロトコル バッファ コンパイラ プラグインを使用してボイラープレート gRPC コードを生成するなど、アプリケーションを完成させる方法について説明します。
このソースコードを GitHub から .ZIP アーカイブとしてダウンロードし、その内容を解凍します。
また、実装の入力をスキップする場合は、完成したソースコードを GitHub で入手できます。
3. サービスを定義する
まず、プロトコル バッファを使用して、アプリケーションの gRPC サービス、RPC メソッド、リクエストとレスポンスのメッセージ タイプを定義します。サービスでは以下を提供します。
- サーバーが実装し、クライアントが呼び出す
GetFeatureという RPC メソッド。 GetFeatureメソッドを使用する際にクライアントとサーバー間で交換されるデータ構造であるメッセージ タイプPointとFeature。クライアントは、サーバーへのGetFeatureリクエストで地図の座標をPointとして提供し、サーバーはそれらの座標にあるものを説明する対応するFeatureで応答します。
この RPC メソッドとそのメッセージ型はすべて、提供されたソースコードの routeguide/route_guide.proto ファイルで定義されます。
Protocol Buffers は一般に protobuf と呼ばれます。gRPC の用語の詳細については、gRPC の コア コンセプト、アーキテクチャ、ライフサイクルをご覧ください。
メッセージのタイプ
ソースコードの routeguide/route_guide.proto ファイルで、まず Point メッセージ タイプを定義します。Point は、地図上の緯度と経度の座標ペアを表します。この Codelab では、座標に整数を使用します。
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
数値 1 と 2 は、message 構造内の各フィールドの一意の ID 番号です。
次に、Feature メッセージ タイプを定義します。Feature は、Point で指定された場所にあるものの名前または郵便番号に string フィールドを使用します。
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
サービス メソッド
route_guide.proto ファイルには、アプリケーションのサービスによって提供される 1 つ以上のメソッドを定義する RouteGuide という名前の service 構造があります。
RouteGuide 定義内に rpc メソッド GetFeature を追加します。前述のとおり、このメソッドは指定された座標セットから場所の名前または住所を検索するため、指定された Point に対して GetFeature が Feature を返すようにします。
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
これは単項 RPC メソッドです。クライアントがサーバーにリクエストを送信し、ローカル関数呼び出しと同様にレスポンスが返ってくるのを待つシンプルな RPC です。
4. クライアント コードとサーバーコードを生成する
次に、プロトコル バッファ コンパイラを使用して、.proto ファイルからクライアントとサーバーの両方のボイラープレート gRPC コードを生成します。routeguide ディレクトリで、次のコマンドを実行します。
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
route_guide.proto
このコマンドは、次のファイルを生成します。
route_guide.pb.go: アプリケーションのメッセージ タイプを作成し、そのデータにアクセスする関数が含まれています。route_guide_grpc.pb.go: クライアントがサービスの gRPC リモート メソッドを呼び出すために使用する関数と、サーバーがそのリモート サービスを提供するために使用する関数が含まれています。
次に、クライアントがリクエストを送信したときにサーバーが回答を返せるように、サーバーサイドで GetFeature メソッドを実装します。
5. サービスを実装する
サーバー側の GetFeature 関数でメインの処理が行われます。この関数は、クライアントから Point メッセージを受け取り、既知の場所のリストから対応する位置情報を Feature メッセージで返します。server/server.go での関数の実装は次のとおりです。
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 呼び出しを記述する Context オブジェクトと、そのクライアント リクエストからの Point プロトコル バッファ オブジェクトが渡されます。この関数は、検索された位置情報の Feature プロトコル バッファ オブジェクトと、必要に応じて error を返します。
メソッドで、指定された Point の適切な情報を使用して Feature オブジェクトを入力し、nil エラーとともに return して、RPC の処理が完了し、Feature オブジェクトをクライアントに返すことができることを gRPC に伝えます。
GetFeature メソッドでは、クライアントからの位置情報検索のリクエストをその関数にルーティングできるように、routeGuideServer オブジェクトを作成して登録する必要があります。これは main() で行われます。
func main() {
flag.Parse()
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...)
s := &routeGuideServer{}
s.loadFeatures()
pb.RegisterRouteGuideServer(grpcServer, s)
grpcServer.Serve(lis)
}
main() で起こっていることをステップごとに説明します。
lis, err := net.Listen(...)を使用して、リモート クライアント リクエストをリッスンするために使用する TCP ポートを指定します。デフォルトでは、アプリケーションは変数portで指定された TCP ポート50051を使用します。または、サーバーの実行時にコマンドラインで--portスイッチを渡すことで指定します。TCP ポートを開けない場合、アプリケーションは致命的なエラーで終了します。grpc.NewServer(...)を使用して gRPC サーバーのインスタンスを作成し、このインスタンスにgrpcServerという名前を付けます。- アプリケーションの API サービスを表す構造体
routeGuideServerへのポインタを作成し、ポインタにs.という名前を付けます。 s.loadFeatures()を使用して、GetFeatureで検索できる場所を配列s.savedFeaturesに入力します。GetFeatureへの RPC 呼び出しが適切な関数に転送されるように、API サービスを gRPC サーバーに登録します。- サーバーでポートの詳細を指定して
Serve()を呼び出し、クライアント リクエストのブロック待機を行います。この処理は、プロセスが強制終了されるかStop()が呼び出されるまで続きます。
関数 loadFeatures() は、座標と位置のマッピングを server/testdata.go から取得します。
6. クライアントを作成する
次に、クライアント コードを実装する client/client.go を編集します。
リモート サービスのメソッドを呼び出すには、まずサーバーと通信するための gRPC チャネルを作成する必要があります。これは、クライアントの main() 関数で、サーバーのターゲット URI 文字列(この場合はアドレスとポート番号)を grpc.NewClient() に渡すことで作成します。
conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
変数 serverAddr で定義されるサーバーのアドレスは、デフォルトでは localhost:50051 です。クライアントの実行時にコマンドラインで --addr スイッチを使用すると、このアドレスをオーバーライドできます。
クライアントが TLS や JWT 認証情報などの認証情報を必要とするサービスに接続する必要がある場合、クライアントは必要な認証情報を含む DialOptions オブジェクトを grpc.NewClient のパラメータとして渡すことができます。RouteGuide サービスには認証情報は必要ありません。
gRPC チャネルを設定したら、Go 関数呼び出しで RPC を実行するためのクライアント スタブが必要です。このスタブは、アプリの .proto ファイルから生成された route_guide_grpc.pb.go ファイルが提供する NewRouteGuideClient メソッドを使用して取得します。
import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")
client := pb.NewRouteGuideClient(conn)
サービス メソッドを呼び出す
gRPC-Go では、RPC はブロッキング/同期モードで動作します。つまり、RPC 呼び出しはサーバーからの応答を待機し、応答またはエラーを返します。
Simple RPC
シンプルな RPC GetFeature の呼び出しは、ローカル メソッド(この場合は client.GetFeature)の呼び出しとほぼ同じくらい簡単です。
point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)
// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
log.Fatalf("client.GetFeature failed: %v", err)
}
クライアントは、前に作成したスタブのメソッドを呼び出します。メソッドのパラメータとして、クライアントは Point リクエスト プロトコル バッファ オブジェクトを作成して入力します。また、必要に応じて RPC の動作を変更できる context.Context オブジェクトも渡します。たとえば、呼び出しの時間制限を定義したり、進行中の RPC をキャンセルしたりできます。呼び出しでエラーが返されなかった場合、クライアントは最初の戻り値からサーバーのレスポンス情報を読み取ることができます。
log.Println(feature)
クライアントの main() 関数は次のようになります。
func main() {
flag.Parse()
// Set up a connection to the gRPC server.
conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
// Create a new RouteGuide stub.
client := pb.NewRouteGuideClient(conn)
point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)
// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
log.Fatalf("client.GetFeature failed: %v", err)
}
log.Println(feature)
}
7. 試してみる
アプリケーションの作業ディレクトリで次のコマンドを実行して、サーバーとクライアントが正しく連携していることを確認します。
- 1 つのターミナルでサーバーを実行します。
cd server go run .
- 別のターミナルからクライアントを実行します。
cd client go run .
次のような出力が表示されます(わかりやすくするため、タイムスタンプは省略されています)。
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:<>
8. 次のステップ
- gRPC の概要とコアコンセプトで gRPC の仕組みを確認する
- 基本チュートリアルに取り組む
- API リファレンスを確認する