【gRPC】.protoファイルを作成し、コンパイルしてGo言語で使用するまで

この記事は約14分で読めます。

gRPCとは?

gRPCは「Google Remote Procedure Call」の略で、Googleが開発したシステム間の通信を効率的に行うためのオープンソースのリモートプロシージャコール(RPC)フレームワーク。

Protocol Buffersは、Googleが開発したデータシリアライゼーションフォーマットで、JSONやXMLなどの伝統的なフォーマットに比べて、データをより小さいサイズで表現し、送受信する速度も速いという特徴がある。これは、データをバイナリ形式で効率的にエンコードし、転送時のデータサイズを最小限に抑えるため。Protocol Buffersを活用する通信フレームワークが「gRPC」。

https://grpc.io/docs/what-is-grpc/introduction/

.protoファイル内でのRPCメソッドの定義方法

サービス定義:

まず、serviceキーワードを使ってサービスを定義する。サービスは一連のRPCメソッドのコレクション。

Go
// test.proto
service MyService {
  // RPCメソッドはここに定義される。
}

RPCメソッド定義:

サービス内に、rpcキーワードを使用してメソッドを定義する。メソッドは入力メッセージタイプと出力メッセージタイプを指定する必要がある。

関数名はリクエスト(リクエストを送るメソッド)にのみ付けられ、レスポンスの型には関数名を付けない。レスポンスはメソッドの戻り値の型として定義されるため。

Go
// test.proto
service MyService {
  // 単一のリクエストとレスポンスを持つRPCメソッド
  rpc MyMethod(MyRequest) returns (MyResponse);
}

メッセージ定義:

RPCメソッドの入力と出力で使用されるメッセージタイプを定義する。

Go
// test.proto
message MyRequest {
  // リクエストパラメータを定義
}

message MyResponse {
  // レスポンスパラメータを定義
}

ストリーミング

https://grpc.io/docs/languages/go/basics/

ストリーミングと単一のリクエスト/レスポンスモデルの違い

gRPCはストリーミングRPCもサポートしている。

従来のRPC通信では、クライアントがサーバーにリクエストを送り、サーバーがそれに対して一つのレスポンスを返す。しかし、ストリーミングを使用すると、一度の接続で複数のインスタンス(同じメッセージ型のデータが複数回、連続してやり取りされること)を送受信できるようになる。

例えば、サーバーストリーミングRPCでは、クライアントは一回のリクエストを送り、サーバーから複数のレスポンスが時系列に沿って順番に送られてくる。

: サーバーがストック価格のリアルタイム情報をクライアントに送信するケース。

Go
message StockRequest {
  string stock_symbol = 1;
}

message StockResponse {
  string stock_symbol = 1;
  float price = 2;
  string timestamp = 3;
}

service StockService {
  rpc GetStockUpdates(StockRequest) returns (stream StockResponse);
}

クライアントがStockRequestを送信し、"AAPL"(Apple Inc.の株式シンボル)に関する更新をリクエストする。サーバーはこれを受け取り、Appleの株価に関するStockResponseメッセージをストリームとして送る。

サーバーからのレスポンスは以下のように連続して送信される。

Go
{ stock_symbol: "AAPL", price: 146.28, timestamp: "2021-07-14T10:00:00Z" }
{ stock_symbol: "AAPL", price: 146.48, timestamp: "2021-07-14T10:01:00Z" }
{ stock_symbol: "AAPL", price: 146.58, timestamp: "2021-07-14T10:02:00Z" }
{ stock_symbol: "AAPL", price: 146.68, timestamp: "2021-07-14T10:03:00Z" }

StockResponseメッセージは、同じメッセージ型の異なるインスタンス。それぞれ異なるデータ(この場合は株価とタイムスタンプ)を持っているが、メッセージ構造は同じ。このようにして、サーバーはリアルタイムで情報をクライアントに送信し続けることができる。クライアントはこれらのメッセージを受け取り、適宜処理を行う。

3つのストリーミングタイプ(クライアント・サーバー・双方向)

ストリーミングには、クライアントストリーミングRPC、サーバーストリーミングRPC、双方向ストリーミングRPCの3つがある。

ストリーミングを使用する場合、メッセージ定義の前にstreamキーワードをつける。

クライアントストリーミングRPC・

  • クライアントがストリームをサーバーに送信し、サーバーが単一のレスポンスを送り返す。大きなデータをサーバーに送信する場合に有用。
  • 例)ファイルのアップロード処理:クライアントからファイルを小さなチャンクに分割して、チャンクをサーバーに順番に送信。サーバーはチャンクを受け取り、元のファイルを再構築する。アップロードが完了すると、サーバーはアップロードの成功や失敗に関する単一のレスポンスをクライアントに送る。
Go
rpc ClientStreaming(stream MyRequest) returns (MyResponse);

サーバーストリーミングRPC:

  • クライアントがリクエストを送信し、サーバーがレスポンスのストリームを送り返す。サーバーからクライアントに連続的なデータを送る場合に有用。
  • 例)動画のストリーミング再生・リアルタイムデータのフィード(株価情報など)
Go
rpc ServerStreaming(MyRequest) returns (stream MyResponse);

双方向ストリーミングRPC

  • クライアントとサーバーが互いにストリームを送受信する。両方向にデータのやり取りが必要な場合に有用。
  • 例)リアルタイムの相互作用が必要なアプリケーション:チャットアプリケーション・リアルタイムのゲームでの通信
Go
rpc BidirectionalStreaming(stream MyRequest) returns (stream MyResponse);

これらの定義を.protoファイルに記述し、protobufコンパイラ(protoc)を使用して、選択した言語のソースコードを生成する。

.protoファイルからGoのコードを生成

メッセージとrpcメソッドを.protoファイルに記述終えたら、protocコマンドを使って、.protoファイルからGoのソースコードを生成する。事前に、protoc-gen-goprotoc-gen-go-grpcプラグインをインストールしておく必要がある。

Go
$ protoc --go_out=. --go-grpc_out=. path/to/yourfile.proto

このコマンドは、指定された.protoファイルからGoのコードを生成し、現在のディレクトリに出力する。--go_out=.および--go-grpc_out=.オプションは、現在のディレクトリにファイルを生成するよう指定。違うディレクトリにファイル生成する場合は変更する。

例えば、ファイルをpbディレクトリ内に生成する場合は下記。

Go
$ protoc --go_out=pb --go-grpc_out=pb path/to/yourfile.proto

例えば、下記example.protoファイルをGo言語用のコードにコンパイルすると、2つのファイルが生成される。

Go
// ファイル名: example.proto
syntax = "proto3";

package pb; //名前空間の衝突を避けるためにpackageを定義

message Request {
  string query = 1;
}

message Response {
  string result = 1;
}

service ExampleService {
  rpc GetResponse(Request) returns (Response);
}

example.pb.go

このファイルには、RequestResponseメッセージに対応するGoの構造体と、それらを操作するための関数(ゲッター関数など)が含まれる。これらはプロトコルバッファの定義から直接生成される。

Protobufは言語に依存しないスキーマ定義から、特定のプログラミング言語のコードを生成することができるツール。生成されるコードは言語に適した形式で、メッセージの構造、シリアライズ/デシリアライズのロジック、アクセッサーなどが含まれる。Go言語の場合は、.protoファイルのメッセージは構造体(struct)としてコンパイルされる。これらの構造体には、フィールドタグが含まれ、Protobufのフィールド番号がマッピングされる。Go言語にはクラスがないが、構造体を使って同様の目的を達成できる。

ちなみに、Dartでは.protoファイルのメッセージ定義は、クラスとしてコンパイルされ、メッセージ内に定義される各フィールドは、クラスのプロパティとしてコンパイルされる。役割としてはGo言語の構造体と同じ。

Go
// example.pb.go(概要)
package pb

type Request struct {
    Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`
}

func (x *Request) Reset() {
    *x = Request{}
}

func (x *Request) String() string {
    return protoimpl.X.MessageStringOf(x)
}

type Response struct {
    Result string `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
}

func (x *Response) Reset() {
    *x = Response{}
}

func (x *Response) String() string {
    return protoimpl.X.MessageStringOf(x)
}

func (m *Request) GetQuery() string {
    if m != nil {
        return m.Query
    }
    return ""
}

example_grpc.pb.go:

このファイルには、ExampleServiceサービスのためのgRPC関連のコードが含まれる。これにはクライアントとサーバーの両方のインターフェース定義が含まれる。サーバー側では、サービスのインターフェースを実装するための基盤が提供され、クライアント側では、サーバーのメソッドを呼び出すための関数が提供される。

Go
// example_grpc.pb.go(概要)
package pb

type ExampleServiceClient interface {
    GetResponse(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}

type ExampleServiceServer interface {
    GetResponse(context.Context, *Request) (*Response, error)
}

func RegisterExampleServiceServer(s *grpc.Server, srv ExampleServiceServer) {
    s.RegisterService(&_ExampleService_serviceDesc, srv)
}

// その他のクライアントやサーバーの実装の詳細

サーバー側でExampleServiceを実装する際には、ExampleServiceServerインターフェースに従って、GetResponseメソッドを実装する。クライアント側では、ExampleServiceClientインターフェースを利用してサーバーのメソッドを呼び出す。

RPCメソッドのGo言語での使用例(Go gRPC API)

gRPCサーバーとクライアントでのGetResponse RPCメソッドの使用例

サーバー側の実装

サーバー側では、ExampleServiceServerインターフェースを実装する必要がある。

pb.protoファイルで定義したパッケージ名に対応しており、生成されたGoコード内でのパッケージ名となる。UnimplementedExampleServiceServerRequestResponseなどの型はすべてpbパッケージに定義される。

Go
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "path/to/your/pb" // pbパッケージへのパスを適切に設定
)

type server struct {
    pb.UnimplementedExampleServiceServer
}

func (s *server) GetResponse(ctx context.Context, in *pb.Request) (*pb.Response, error) {
    log.Printf("Received: %v", in.GetQuery())
    return &pb.Response{Result: "Hello " + in.GetQuery()}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterExampleServiceServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

クライアント側の実装

クライアント側では、サーバーに接続してGetResponseメソッドを呼び出す。

このクライアントのコードでは、サーバーに接続し、GetResponse RPCを呼び出して、”world”というクエリに対するレスポンスを取得している。サーバーはこのクエリに”Hello”を追加して返すため、クライアントは”Hello world”という結果を受け取り、ログに出力する。

Go
package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    pb "path/to/your/pb" // pbパッケージへのパスを適切に設定
)

func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewExampleServiceClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.GetResponse(ctx, &pb.Request{Query: "world"})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetResult())
}

コメント

タイトルとURLをコピーしました