2020-02-06

Go で gRPC を使うためのまとめ

gRPCを使いそうなので記事にまとめておきます。

ちょっとだけ長いので必要なところだけ読めばいいと思います。 あとスマホで見ようとすると重いのでPCで見ることをおすすめします。

今回使うコードはリポジトリに置いてあります。 GitHub - righ/grpc-go-exampleContribute to righ/grpc-go-example development by creating an account on GitHub.https://github.com/righ/grpc-go-example

これはgrpc/grpc-go/examples をベースに修正と追記をしたものです。

clone して docker-compose すればコンテナが2つ起動します。

$ git clone https://github.com/righ/grpc-go-example/ $ cd grpc-go-example/ $ docker-compose up Creating network "grpc_default" with the default driver Creating go2 ... done Creating go1 ... done Attaching to go2, go1

以下の内容はすべてこれらのコンテナが起動している前提なので、手元で試したい方は clone してください。

🌀 とりあえず使ってみる

起動した2つのコンテナ(grpc-server,grpc-client)に入って プログラムをインストールします。

インストールしたプログラムは $GOPATH/bin/ に配置されるのでそのまま実行できます。

  • grpc-server
  • grpc-client
  • $ docker exec -it grpc-server /bin/bash root@82769e87d925:~# go get -u github.com/righ/grpc-go-example/helloworld/greeter_server root@82769e87d925:~# greeter_server 2020/02/01 12:34:44 Received: world 2020/02/01 12:34:51 Received: world
  • $ docker exec -it grpc-client /bin/bash root@189290506d75:~# go get -u github.com/righ/grpc-go-example/helloworld/greeter_client root@189290506d75:~# greeter_client 2020/02/01 12:34:44 Greeting: Hello world root@189290506d75:~# greeter_client 2020/02/01 12:34:51 Greeting: Hello world

クライアントプログラムを実行するたびに以下の処理が実行されているだけです。

  • リクエストを受け取ったサーバはその内容を表示
  • クライアントは返却されたレスポンスを表示
info
  • 以降のプログラムも、サーバは grpc-server コンテナ、 クライアントは grpc-client コンテナで動くことを前提に書いています。

この程度の通信プログラムくらいなら自力でも簡単に書くことができますよね。

何故 gRPC を使うと嬉しいのか。 gRPC 自体が高速だというのもあるのですが、別の理由としては Protocol Buffer の存在が大きいです。

📖 Protocol Buffer

まずは簡単な説明を。

Protocol Buffers(プロトコルバッファー)はインタフェース定義言語 (IDL) で構造を定義する通信や永続化での利用を目的としたシリアライズフォーマットであり、Googleにより開発されている。

Protocol Buffers - Wikipedia

gRPCはこの Protocol Buffer で定義された構造を使って通信を行います。

定義は下記(左)のようなテキストファイルです(拡張子を proto とするのが慣例)

この定義をもとにコードを自動生成(上記右)して、自動生成されたコード(パラメータなど)を使って実装することで、定義した構造を用いて通信できるというわけです。

このコードを自動生成するのが protoc コマンドです。

Macであれば brew を使ってインストールすることもできますが、 それ以外の環境を利用する場合は Releases からダウンロードしてきましょう

今回は 簡単なスクリプト を使ってインストールします。これで protoc コマンドが使えるようになります。

root@2b588693c971:~# ./protoc_install.sh root@2b588693c971:~# protoc --version libprotoc 3.11.2 # protoファイルが置いてあるディレクトリに移動 root@2b588693c971:~# cd helloworld/helloworld root@2b588693c971:~/helloworld/helloworld# protoc --go_out=plugins=grpc:. helloworld.proto # 指定したディレクトリに `ファイル名.pb.go` が作られる root@2b588693c971:~/helloworld/helloworld# ls helloworld.pb.go helloworld.proto # オプション root@2b588693c971:~/helloworld/helloworld# protoc -help Usage: protoc [OPTION] PROTO_FILES Parse PROTO_FILES and generate output based on the options given: -IPATH, --proto_path=PATH Specify the directory in which to search for imports. May be specified multiple times; directories will be searched in order. If not given, the current working directory is used. If not found in any of the these directories, the --descriptor_set_in descriptors will be checked for required proto file. --version Show version info and exit. -h, --help Show this text and exit. --encode=MESSAGE_TYPE Read a text-format message of the given type from standard input and write it in binary to standard output. The message type must be defined in PROTO_FILES or their imports. --decode=MESSAGE_TYPE Read a binary message of the given type from standard input and write it in text format to standard output. The message type must be defined in PROTO_FILES or their imports. --decode_raw Read an arbitrary protocol message from standard input and write the raw tag/value pairs in text format to standard output. No PROTO_FILES should be given when using this flag. --descriptor_set_in=FILES Specifies a delimited list of FILES each containing a FileDescriptorSet (a protocol buffer defined in descriptor.proto). The FileDescriptor for each of the PROTO_FILES provided will be loaded from these FileDescriptorSets. If a FileDescriptor appears multiple times, the first occurrence will be used. -oFILE, Writes a FileDescriptorSet (a protocol buffer, --descriptor_set_out=FILE defined in descriptor.proto) containing all of the input files to FILE. --include_imports When using --descriptor_set_out, also include all dependencies of the input files in the set, so that the set is self-contained. --include_source_info When using --descriptor_set_out, do not strip SourceCodeInfo from the FileDescriptorProto. This results in vastly larger descriptors that include information about the original location of each decl in the source file as well as surrounding comments. --dependency_out=FILE Write a dependency output file in the format expected by make. This writes the transitive set of input file paths to FILE --error_format=FORMAT Set the format in which to print errors. FORMAT may be 'gcc' (the default) or 'msvs' (Microsoft Visual Studio format). --print_free_field_numbers Print the free field numbers of the messages defined in the given proto files. Groups share the same field number space with the parent message. Extension ranges are counted as occupied fields numbers. --plugin=EXECUTABLE Specifies a plugin executable to use. Normally, protoc searches the PATH for plugins, but you may specify additional executables not in the path using this flag. Additionally, EXECUTABLE may be of the form NAME=PATH, in which case the given plugin name is mapped to the given executable even if the executable's own name differs. --cpp_out=OUT_DIR Generate C++ header and source. --csharp_out=OUT_DIR Generate C# source file. --java_out=OUT_DIR Generate Java source file. --js_out=OUT_DIR Generate JavaScript source. --objc_out=OUT_DIR Generate Objective C header and source. --php_out=OUT_DIR Generate PHP source file. --python_out=OUT_DIR Generate Python source file. --ruby_out=OUT_DIR Generate Ruby source file. @<filename> Read options and filenames from file. If a relative file path is specified, the file will be searched in the working directory. The --proto_path option will not affect how this argument file is searched. Content of the file will be expanded in the position of @<filename> as in the argument list. Note that shell expansion is not applied to the content of the file (i.e., you cannot use quotes, wildcards, escapes, commands, etc.). Each line corresponds to a single argument, even if it contains spaces.
warning
  • このスクリプトの中では Goのコードを生成するための protoc-gen-go も同時にインストールしています
  • これがない状態でコードを生成しようとすると次のようなエラーになるので手動でインストールする場合は注意してください。
  • protoc-gen-go: program not found or is not executable
syntax
  • Protocol Buffer のバージョン
  • 現時点(2020-02)では 3 が最新。
option
  • protoc に与えるオプション

  • 現状 Golang 向けのオプションは go_package だけっぽいです。

  • info
    • option go_package = "../ptypes"; のように指定して以下のように実行すれば ../ptypes/ に pb ファイルが出力されます。
    • ~/helloworld/helloworld# protoc --go_out=plugins=grpc:. helloworld.proto ~/helloworld/helloworld# ls ../ptypes/ helloworld.pb.go
    • ちなみに --go_out=plugins=grpc:{パス} を起点に出力されるため go_package にフルパスを指定するなら、パスには / を指定します。
  • 参考

package
  • パッケージ名
  • (Golangでは)Goファイルのパッケージ名になるため出力先のディレクトリ名と合わせるべき
import
  • 別ファイルの定義を取り込む。
message
service
  • 機能をまとめた単位。サービスには RPC が紐づく。
  • (Golangでは)定義したServiceの数だけ Server, Client のインタフェースが作られる
  • Serviceの rpc フィールドはそれぞれのインタフェースのメソッドとして登録される
参考

⭐️ RPC types

gRPCは4種類の通信方法を提供します。

というと身構えてしまいそうですが、 簡単に言うと「 stream を指定するかしないか」 「クライアント側で指定するかサーバ側で指定するか」で名称が分かれているだけです。

  • 種類
  • stream指定
  • Unary RPC
  • sequenceDiagram participant client participant server client->>server: Request server-->>client: Response
  • Server streaming RPC
  • サーバー
  • sequenceDiagram participant client participant server client->>server: Request server-->>client: Response server-->>client: Response
  • Client streaming RPC
  • クライアント
  • sequenceDiagram participant client participant server client->>server: Request client->>server: Request server-->>client: Response
  • Bidirectional streaming
  • 両方
  • sequenceDiagram participant client participant server client->>server: Request server-->>client: Response server-->>client: Response client->>server: Request client->>server: Request client->>server: Request

ではストリーミングとは何かというと、 連続した通信(リクエスト,レスポンス)で、これまでの単発の通信と対比する呼称です。

gRPCはリクエスト、レスポンスともにデータサイズに上限があります。やり取りするデータが可変長で巨大になる可能性がある場合はストリーミングによって通信を分割することが望ましいです。

🌚 Unary RPC

1つのリクエストに対して1つのレスポンスを返す最も基本的なRPCです。

Unary RPC - gRPC

クライアント、サーバともに stream を指定しなければデフォルトでこれになります。

先程も見ましたが、定義ファイルをもう一度確認しておきましょう。

これらを使ってサーバとクライアントのコードを書いていきます。

  • これは以降全てに共通することですが、サーバ側のコードでは pb.Register{Service名}Server 関数を使って「grpcのサーバ」と「サービス」を紐付けるわけですが、 サービスには上述したインタフェースを引数に取るため、構造体には rpc に相当するメソッドが定義されている必要があります。

    Unary RPC では サーバ側のRPCメソッド はリクエストのパラメータとして構造体を受け取って レスポンスとして構造体を返却するだけです。

  • Unary RPC のクライアント側コードは更にシンプルで pb.New{Service名}Client で作成したクライアントから呼び出したい rpc のメソッドを呼び出すだけです。

    サーバと違い、レスポンスを返却値として返すだけなのでメソッドを実装する必要はありません。

これは Unary RPC に限った話ではないですが、サーバ側のインタフェースを満たす空の構造体(Unimplemented{サービス名}Server)が自動的に生成されるので、 それを使えばひとまずサーバとして動くようになります。

今回のexampleのように、これ(UnimplementedServer)を埋め込んだ構造体を自分で定義して、 必要なメソッドを追加していくというのも良さそうです。

type server struct { pb.UnimplementedGreeterServer }

実装すべき機能が多い場合は便利ですね。

warning
  • Unary RPC は最も基本的な gRPC ではありますが、後述する RPC と引数が異なるので注意してください。

利用例は最初のセクションを参照してください。

🌜 Server streaming RPC

1つのリクエストに対して、複数(N)のレスポンスを返します。

Server streaming RPC - gRPC

まずはproto定義と自動生成ファイル。 レスポンスにだけ stream が指定されています。

今回プログラムの概要は以下です。

  • クライアントは名前をサーバに一度だけ送信する
  • サーバは受け取った名前を使って挨拶を組み立てて3回連続で返却する
    • デバッグで標準出力にも表示する
  • クライアントはサーバから受け取ったメッセージを標準出力に表示する

実用性は全くありません。

  • Server streaming RPC の サーバ側のRPCメソッド は Unary RPC とは少し違い、リクエストパラメータは第1引数で受け取ります。 レスポンスは第2引数のServer構造体(以下 srv という)の srv.Send メソッドを使って複数返却できます。

    ctx はどこいったの?となりますが srv.Context に入っています。

  • Server streaming PRC の クライアント側のRPCメソッドも Unary RPC とは違います。 具体的にはパラメータは引数として受け取らず、メソッドの返却値としてレスポンスではなくRPCのクライアントが返却されます。(以下 cli という)

    cli.Send メソッドを使いリクエストを送信し(パラメータはここで指定する)、 cli.Recv メソッドを使って res, err := cli.Recv() のようにレスポンスを抽出します。 レスポンスは複数受け取ることになるためループの中で受け取ることになりますが、何らかの形で終了を検知しなければなりません。

    これは err が io.EOF と一致するかどうかで判定できるので検知したらループを抜けます。

  • ~/goodnightworld/greeter_server# go run .
  • ~/goodnightworld/greeter_client# go run .
  • 2020/02/01 08:13:23 Received: name:"righ"
  • 2020/02/01 08:13:23 Good night righ! 2020/02/01 08:13:23 Good night righ! 2020/02/01 08:13:23 Good night righ!
sequenceDiagram participant client participant server client->>server: righ server-->>client: Good night righ! server-->>client: Good night righ! server-->>client: Good night righ!

🌛 Client streaming RPC

複数(M)のリクエストに対して、1つのレスポンスを返します。

Client streaming RPC - gRPC

まずはproto定義と自動生成ファイル。 リクエストにだけ stream が指定されています。

今回作成するプログラムの概要は以下です。

  • クライアントは標準入力から名前を受け取り、一つずつサーバに送信する
    • 空行が入力されるとクライアント側の入力は完了とする
  • サーバはクライアントから受け取った名前を使い挨拶を組み立ててクライアントに返却する
    • 受け取った名前をデバッグ用に標準出力に表示する
  • クライアントはサーバから受け取った挨拶を標準出力に表示する

実用性はない。

  • Client streaming RPC のサーバ側RPCメソッドは Server構造体(以下 srv という)を引数として、 srv.Recv メソッドを使い req, err := srv.Recv() のようにリクエストを抽出するように実装します。

    リクエストは複数受け取ることになるため、先程のクライアントサイドと同様に err が io.EOF のときにループを抜けるようにします。

    レスポンスは srv.SendAndClose を使って返却します。

  • Client streaming PRC の クライアント側RPCメソッドはRPCのクライアントが返却されます。(以下 cli という)

    cli.Send メソッドを使いリクエストを送信し(パラメータはここで指定する)、 cli.CloseAndRecv メソッドを使って res, err := cli.CloseAndRecv() のようにサーバからのレスポンスを抽出し、通信を終了します。

  • ~/goodmorningworld/greeter_server# go run .
  • ~/goodmorningworld/greeter_client# go run .
  • 2020/02/01 08:50:44 Received: name:"dj" 2020/02/01 08:50:44 Received: name:"steph" 2020/02/01 08:50:44 Received: name:"michelle"
  • root@98337ba2f5a0:~/goodmorningworld/greeter_client# go run . dj steph michelle Good morning dj,steph,michelle!
sequenceDiagram participant client participant server client->>server: dj client->>server: setph client->>server: michelle server-->>client: Good morning dj,steph,michelle!

🌝 Bidirectional streaming RPC

複数(N)のリクエストに対して、複数(M)のレスポンスを返すことで双方向の通信を実現します。

Bidirectional streaming RPC - gRPC

まずはproto定義と自動生成ファイル。

RPCのリクエスト・レスポンスともに stream を指定しました。

双方向ということですが、私の想像力が乏しいせいで例題がチャットくらいしか思い浮かばなかったのでかなり不完全ではありますがとりあえず作ってみました。

今回作成するプログラムの概要は以下のようになっています。

  • クライアントは 標準入力から受け取ったメッセージ自分の名前 をサーバに送信する
    • 自分の名前はクライアントプログラム開始時にオプションで指定する
  • サーバはクライアントから受信した 名前メッセージ受信時刻 とともに保持する
    • デバッグ用に標準出力にも表示する
  • サーバはクライアントからメッセージを受信するたびに、クライアントごとに保持している最終受信時刻より後に受け取ったメッセージをすべてクライアントに送信する
  • クライアントはサーバから受け取ったメッセージを標準出力に表示する

続いてプログラム。これまでの中では一番複雑ですが、せいぜい60行くらいです。

  • Bidirectional streaming RPC のサーバ側のRPCメソッドは Server構造体(以下 srv という)を受け取るように実装します。

    リクエストを複数受け取るためループ内で srv.Recv メソッドを使い req, err := srv.Recv() のようにリクエストを抽出します。

    レスポンスは srv.Send メソッドを使って複数返却できます。1個でもいいし、返却しなくても良いでしょう。

  • Bidirectional streaming PRC の クライアント側RPCメソッドはRPCのクライアントを返却します。(以下 cli という)

    リクエストは複数送信できるためループ内で cli.Send メソッドを使い送信し(パラメータはここで指定する)、 レスポンスも複数受信するためループ内で cli.Recv メソッドを使って res, err := cli.Recv() のように抽出します。

    今回は 「送信(入力)」と「受信」の両方を待ち受ける必要があるため、「送信(入力)」の方をゴルーチンでバックグラウンド実行させています。

多少チャットらしくするため、今回はクライアントを2つ起動して残念な会話を繰り広げてみます

  • ~/chat/chat_server# go run .
  • ~/chat/chat_client# go run . -name Karen
  • ~/chat/chat_client# go run . -name Yoko
  • 2020/02/01 07:36:46 Karen>: はじめまして 2020/02/01 07:36:49 Karen>: かれんです 2020/02/01 07:36:54 Yoko>: こんにちは 2020/02/01 07:36:59 Yoko>: 用高です 2020/02/01 07:37:41 Karen>: なんて呼んだらいいですか? 2020/02/01 07:37:52 Yoko>: かれんさん、いい名前ですね!
  • はじめまして かれんです なんて呼んだらいいですか? 2020/02/01 07:37:41 Yoko> こんにちは 2020/02/01 07:37:41 Yoko> 用高です
  • こんにちは 2020/02/01 07:36:54 Karen> はじめまして 2020/02/01 07:36:54 Karen> かれんです 用高です かれんさん、いい名前ですね! 2020/02/01 07:37:52 Karen> なんて呼んだらいいですか?

(会話はここで途切れている

sequenceDiagram participant client1 participant server participant client2 client1->>server: はじめまして client1->>server: かれんです client2->>server: こんにちわ client2->>server: 用高です client1->>server: なんて呼んだらいいですか? server->>client1: こんにちわ server->>client1: 用高です client2->>server: かれんさん、いい名前ですね! server->>client2: なんて呼んだらいいですか?

お気付きの通り、双方向通信ではあるもののリアルタイム通信ではないので使い勝手はお世辞にも良いとは言えません。

本来はメッセージが届いた時点でつながっている全クライアントにブロードキャストできたらよかったんですが、 gRPCは現状で 任意 のクライアントに対してデータを送信する機能を提供していません。

サーバはクライアントからRPC接続を受けるたびにゴルーチンを作り、該当するRPCメソッドはバックグラウンド実行されます。 ここで作られたゴルーチンは接続してきたクライアントしか知らないので、現実装のRPCでは「受信(srv.Recv)」が唯一のメッセージ同期のトリガーとなってしまっているわけです。

🌞 改良する

(このセクションはgRPCとはそこまで関係ないので読み飛ばしてOKです)

じゃあどうすればよいのか。 一言でいうと「送信と受信のRPCを分ける」が答えです。

今回は先ほどとは趣向を変えて、チャネルをメッセージキューのように使ってみようと思います。

というわけで「メッセージ送信RPC」「メッセージ受信RPC」「チャネル作成RPC」を用意します。

今回のプログラムの概要は以下のようになっています。

  • クライアントは「チャネル作成RPC(Unary RPC)」経由でサーバにアクセスする
    • サーバ側はIDとチャネルの組を作成し、クライアントにIDを返却する
  • クライアントは標準入力から入力されたメッセージと名前を ID とともに「メッセージ送信RPC(Client streaming RPC)」に送信する
    • サーバは受け取った ID紐付かない チャネル全てに対し、受け取った「名前とメッセージ」を送信する
      • このときクライアントには何も返却しない
  • クライアントは「メッセージ受信RPC(Server streaming RPC)」に ID を送信し、サーバからメッセージを受信するたびに標準出力に表示する
    • この処理はクライアント側でもバックグラウンド実行 され続ける
    • サーバは受け取った ID紐づく チャネルからメッセージを取り出すたびにクライアントに送信する

今回のプログラムでは CreateChannel RPCにアクセスするたびにチャネルを作成し、 それに対応するIDをクライアントに伝えます。いわばセッションIDのようなものです(セキュリティは度外視)。

クライアントはこのIDとメッセージを一緒にメッセージ送信RPCに送信し、受け取ったサーバはIDに紐付かない(つまり自分以外の)チャネルにメッセージを送信します。

クライアントはこれに並行してメッセージ受信RPCにアクセスしておきます。サーバ側のメッセージ受信RPCはチャネルにメッセージが送られてきたタイミングでクライアントにレスポンスを返却するため、リアルタイムでメッセージが送受信されているように見えるというわけですね。

ただし、このプログラムはリソース競合やメモリリークを考慮していないのでそのまま使うのはおすすめできません。

  • ~/chat2/chat_server# go run .
  • ~/chat2/chat_client# go run . -name Karen
  • ~/chat2/chat_client# go run . -name Yoko
  • 2020/02/01 16:58:21 Oji> かれんちゃん、オッハー😃♥ 😃✋😍ちょっと電話できるカナ😜⁉️✋❓❗❓水曜日、会社がお休みになった、よ(<)😆かれんちゃんは都合どうかな( ̄ー ̄?) ドライブ🚗どウ(<)😃♥ ナンチャッテ(^_^)(^o^)❗(笑) 2020/02/01 16:58:47 Karen> 水曜日は風邪引くから無理かも! 2020/02/01 16:59:16 Oji> くれぐれも体調に気をつけて( ̄▽ ̄)(^^ 🤑😤ゆっくり、身体休めテネ(笑)オヤスミナサイ🙂
  • 2020/02/01 16:58:21 Oji> かれんちゃん、オッハー😃♥ 😃✋😍ちょっと電話できるカナ😜⁉️✋❓❗❓水曜日、会社がお休みになった、よ(<)😆かれんちゃんは都合どうかな( ̄ー ̄?)ドライブ🚗どウ(<)😃♥ ナンチャッテ(^_^)(^o^)❗(笑) 水曜日は風邪引くから無理かも! 2020/02/01 16:59:16 Oji> くれぐれも体調に気をつけて( ̄▽ ̄)(^^ 🤑😤ゆっくり、身体休めテネ(笑)オヤスミナサイ🙂
  • かれんちゃん、オッハー😃♥ 😃✋😍ちょっと電話できるカナ😜⁉️✋❓❗❓水曜日、会社がお休みになった、よ(<)😆かれんちゃんは都合どうかな( ̄ー ̄?)ドライブ🚗どウ(<)😃♥ ナンチャッテ(^_^)(^o^)❗(笑) 2020/02/01 16:58:47 Karen> 水曜日は風邪引くから無理かも! くれぐれも体調に気をつけて( ̄▽ ̄)(^^ 🤑😤ゆっくり、身体休めテネ(笑)オヤスミナサイ🙂
sequenceDiagram participant client1 participant server participant client2 client2->>server: かれんちゃん、オッハー😃♥ 😃✋😍<br/>ちょっと電話できるカナ😜⁉️✋❓❗❓<br/>水曜日、会社がお休みになった、よ(^з<)😆<br/>かれんちゃんは都合どうかな( ̄ー ̄?)<br/> ドライブ🚗どウ(^з<)😃♥ <br/>ナンチャッテ(^_^)(^o^)❗(笑) server->>client1: かれんちゃん、オッハー😃♥ 😃✋😍<br/>ちょっと電話できるカナ😜⁉️✋❓❗❓<br/>水曜日、会社がお休みになった、よ(^з<)😆<br/>かれんちゃんは都合どうかな( ̄ー ̄?)<br/> ドライブ🚗どウ(^з<)😃♥ <br/>ナンチャッテ(^_^)(^o^)❗(笑) client1->>server: 水曜日は風邪引くから無理かも! server->>client2: 水曜日は風邪引くから無理かも! client2->>server: くれぐれも体調に気をつけて( ̄▽ ̄)(^^ 🤑😤<br/>ゆっくり、身体休めテネ(笑)オヤスミナサイ🙂 server->>client1: くれぐれも体調に気をつけて( ̄▽ ̄)(^^ 🤑😤<br/>ゆっくり、身体休めテネ(笑)オヤスミナサイ🙂

今回はちゃんと会話が噛み合っていますね!

👽 Meta data

メタデータは構造化されていない通信データです。

メインではない何らかの付加情報をやり取りするときに使います。 といってもあまり良い例が思い浮かびませんが、ログなどに記録されるタイムスタンプなどでしょうか。

既存のにメタデータを使ったexampleがあったのでそれを使います。(対象ホストだけ変更)

まずは定義から。この時点で変わったところはありません。

以下、プログラムです。 "google.golang.org/grpc/metadata" を使うのがミソですね。

  • サーバはコンテキスト経由でメタデータを受け取ります。 md, ok := metadata.FromIncomingContext(ctx) のようにコンテキストからメタデータを取得します。 メタデータはマップ型なので md["timestamp"] のように値を抽出します

    クライアントに送信する場合は、 SendHeader か SetTrailer メソッドを使います。

    レスポンスの前に送るメタデータが Header, あとに送るメタデータが Trailer ということらしいです。 これは gRPC というより HTTP2 の用語です。 (gRPCにおけるmetadata、そしてそれを node.js client から取得する - Qiita)

  • クライアントからメタデータを送信するにはコンテキストを使います。 メタデータ自体は metadata.Pairs で作り、 metadata.NewOutgoingContext(context.Background(), md) でメタデータを紐付けたコンテキストを作成します。

    受信する方法ですが、Unary RPC の場合は ...grpc.CallOption に複数指定します。 Headerを受け取るときは grpc.Header(&header), Trailer を受け取るときは grpc.Trailer(&trailer) のように metadata.MD 変数のアドレスを渡すことでサーバからのメタデータを受け取ります。

    Streaming RPC の場合は stream.Header()stream.Trailer() のように得られます。

とりあえず使ってみます。

  • ~/features/metadata/server# go run .
  • ~/features/metadata/client# go run .
  • server listening at [::]:50051 --- UnaryEcho --- timestamp from metadata: 0. Feb 1 17:27:41.704179000 request received: message:"this is examples/metadata" , sending echo --- ServerStreamingEcho --- timestamp from metadata: 0. Feb 1 17:27:42.761855500 request received: message:"this is examples/metadata" echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata echo message this is examples/metadata --- ClientStreamingEcho --- timestamp from metadata: 0. Feb 1 17:27:43.764238600 request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo request received: message:"this is examples/metadata" , building echo echo last received message --- BidirectionalStreamingEcho --- timestamp from metadata: 0. Feb 1 17:27:44.769518100 request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo request received message:"this is examples/metadata" , sending echo
  • --- unary --- timestamp from header: 0. Feb 1 17:27:41.717621700 location from header: 0. MTV response: - this is examples/metadata timestamp from trailer: 0. Feb 1 17:27:41.717779600 --- server streaming --- timestamp from header: 0. Feb 1 17:27:42.763338200 location from header: 0. MTV response: - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata timestamp from trailer: 0. Feb 1 17:27:42.763500200 --- client streaming --- timestamp from header: 0. Feb 1 17:27:43.764817300 location from header: 0. MTV response: - this is examples/metadata timestamp from trailer: 0. Feb 1 17:27:43.767922700 --- bidirectional --- response: timestamp from header: 0. Feb 1 17:27:44.771782200 location from header: 0. MTV - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata - this is examples/metadata timestamp from trailer: 0. Feb 1 17:27:44.772841200

出力が多くてちょっと分かりづらいですが、 メタデータがやり取りできてるっぽいです。

使い分けを簡単にまとめ。ストリーミングは全部同じですが..

種類
    • サーバ
    • クライアント
Unary RPC
      • header
        • grpc.SetTrailer(ctx, trailer)
      • trailer
        • grpc.SendHeader(ctx, header)
      • c.UnaryEcho(ctx, &pb.EchoRequest{Message: message}, grpc.Header(&header), grpc.Trailer(&trailer))
Server streaming RPC
      • header
        • stream.SendHeader(header)
      • trailer
        • stream.SetTrailer(trailer)
      • header
        • header := stream.Header()
      • trailer
        • trailer := stream.Trailer()
Client streaming RPC
      • header
        • stream.SendHeader(header)
      • trailer
        • stream.SetTrailer(trailer)
      • header
        • header := stream.Header()
      • trailer
        • trailer := stream.Trailer()
Bidiretional stream RPC
      • header
        • stream.SendHeader(header)
      • trailer
        • stream.SetTrailer(trailer)
      • header
        • header := stream.Header()
      • trailer
        • trailer := stream.Trailer()

流石に疲れたのでここまで

テストは別の記事にするかもしれません。(しれないかもしれません)

間違いがあったら優しく教えて下さい。

参考