Go@gRPCクライアントパッケージ¶
はじめに¶
本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。
01. セットアップ¶
各種ツール¶
▼ Protocol Bufferコンパイラー¶
proto
ファイルをコンパイルするために、Protocol Bufferコンパイラーをインストールする。
マイクロサービス別にリポジトリがある場合、各リポジトリで同じバージョンのprotoc
コマンドを使用できるように、パッケージ管理ツールを使用した方がよい。
$ asdf plugin list all | grep protoc
$ asdf plugin add protoc https://github.com/paxosglobal/asdf-protoc.git
$ asdf install protoc
▼ Protocol BufferコンパイラーGoプラグイン¶
サービス定義ファイル (proto
ファイル) からpb.go
ファイルをコンパイルするために、Protocol Bufferコンパイラーのプラグインをインストールする。
ただし、執筆時点 (2024/07/15) では、 protoc-gen-go
モジュールに移行するべきである。
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@HEAD
$ protoc-gen-go --version
protoc-gen-go <バージョン>
▼ Protocol BufferコンパイラーGo-gRPCプラグイン¶
サービス定義ファイル (proto
ファイル) からgRPC対応のpb.go
ファイルをコンパイルするために、Protocol Bufferコンパイラーのプラグインをインストールする。
protoc-gen-go
の移行先のモジュールである。
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@HEAD
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc <バージョン>
▼ 上記ツールを持つ専用コンテナ¶
各種ツールをGoアプリにダウンロードしても良いが、専用コンテナとして切り分けるとよい。
サービス定義ファイル (proto
ファイル) からpb.go
ファイルを作成したくなったら、このコンテナを実行する。
docker-compose.yml
ファイルは以下の通りである。
services:
grpc_compile:
image: protocol_buffer_compiler
build:
context: .
container_name: protocol_buffer_compiler
volumes:
- .:/
Dockerfile
ファイルは以下の通りである。
FROM golang:<Goアプリと同じバージョン>
RUN apt update -y \
&& apt install -y protobuf-compiler \
&& export GO111MODULE=on \
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@<バージョン> \
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@<バージョン>
COPY . /
CMD ["/protocol_buffer_compiler.sh"]
protocol_buffer_compiler.sh
ファイルは以下の通りである。
バックアップも兼ねて、pb.go
ファイルを作業日付ごとに作成する。
#!/bin/sh
# 日付ごとにディレクトリを作成する
DATE=`date '+%Y%m%d%H%M%S'`
mkdir -p ${DATE}
protoc \
-I=${DATE} \
--go_out=${DATE} \
--go_opt=paths=source_relative \
--go-grpc_out=${DATE} \
--go-grpc_opt=paths=source_relative,require_unimplemented_servers=false \
*.proto
gRPCクライアントとgRPCサーバーの両方¶
▼ proto
ファイル (サービス定義ファイル) とは¶
gRPCクライアントとgRPCサーバーの両方で、サービス定義ファイル (proto
ファイル) を作成する。
例えば、message
はJSONに代わるスキーマを表し、service
はgRPCにおけるAPI仕様を表す。
サービス定義ファイルにインターフェースとメッセージ構造を実装し、このファイルからpb.go
ファイルをコンパイルする。
▼ pb.go
ファイルとは¶
gRPCクライアントとgRPCサーバーの両方で、proto
ファイルからpb.go
ファイルをコンパイルする。
protoc
コマンドを使用して、pb.go
ファイルを作成する。
このファイルには、gRPCクライアントとgRPCサーバーの両方が参照するための実装が定義されており、開発者はそのまま使用すれば良い。
# foo.protoファイルから、gRPCに対応するfoo.pb.goファイルをコンパイルする。
$ protoc -I=. --go_out=. --go-grpc_out=. foo.proto
# ワイルドカードで指定できる。
$ protoc -I=. --go_out=. --go-grpc_out=. *.proto
▼ RPC-API仕様書¶
gRPCにおけるAPI仕様書である。
仕様の実装であるproto
ファイルを使用して、RPC-API仕様書を作成できる。
$ protoc --doc_out=. --doc_opt=html,index.html *.proto
サーバー側のみ¶
▼ gRPCサーバー¶
リモートプロシージャーコールを受け付けるサーバーを定義する。
サーバーをgRPCサーバーとして登録する必要がある。
gRPCクライアント側のみ¶
▼ gRPCクライアントパッケージ¶
記入中...
▼ gRPCクライアント¶
GoのgRPCサーバーをリモートプロシージャーコールする。
02. クライアント側のインターセプター¶
クライアント側のインターセプターとは¶
gRPCでは、ミドルウェア処理として、インターセプターをリクエスト処理の前後に挿入する。
インターセプターの種類¶
▼ メトリクス系¶
package main
import (
"google.golang.org/grpc"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
)
func main() {
...
// gRPCサーバーとのコネクションを作成する
conn, err := grpc.DialContext(
ctx,
":7777",
grpc.WithChainUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor),
)
...
}
▼ 分散トレース系¶
package main
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
func main() {
...
// gRPCサーバーとのコネクションを作成する
conn, err := grpc.DialContext(
ctx,
":7777",
grpc.WithChainUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
)
...
}
02-02. インターセプターの設定方法¶
単項RPCの場合¶
▼ 既製のインターセプター (UnaryClientInterceptor
)¶
gRPCでは、単項RPCを送信するクライアント側のミドルウェア処理はUnaryClientInterceptor
という名前で定義されている。
type UnaryClientInterceptor func(
ctx context.Context,
method string,
req,
reply interface{},
cc *ClientConn,
invoker UnaryInvoker,
opts ...CallOption
) error
この関数は、リクエストでエラーが起こった場合に、内部的にSetStatus
関数を実行する。
エラー時に、Spanステータスとエラーメッセージをスパンに設定してくれる。
これをgRPCサーバーとのコネクション作成時に、WithChainUnaryInterceptor
関数に渡す。
package main
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
// pb.goファイルを読み込む。
pb "github.com/hiroki-hasegawa/foo/foo"
)
func main() {
...
// gRPCサーバーとのコネクションを作成する
conn, err := grpc.DialContext(
ctx,
":7777",
grpc.WithChainUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
)
defer conn.Close()
// gRPCクライアントを作成する
client := pb.NewFooServiceClient(conn)
// goサーバーをリモートプロシージャーコールする。
response, err := client.SayHello(
context.Background(),
&pb.Message{Body: "Hello From Client!"},
)
...
}
▼ 自前のインターセプター¶
関数に定められた引数を定義すれば、インターセプターとして使用できる。
package intercetor
import (
"google.golang.org/grpc"
)
func UnaryClientInterceptor(
ctx context.Context,
method string,
req,
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
callOpts ...grpc.CallOption,
) error {
// リクエスト送信の事前処理
err := invoker(ctx, method, req, reply, cc, callOpts...) // 単項RPC処理
// リクエスト送信の事後処理
return err
}
ユーザー定義のオプションを渡したい場合は、grpc.UnaryClientInterceptor
型を返却する関数とする。
package intercetor
import (
"google.golang.org/grpc"
)
func UnaryClientInterceptor(opts ...fooOption) grpc.UnaryClientInterceptor {
// オプションを使用してパラメーターを作成する
return func(
ctx context.Context,
method string,
req,
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
callOpts ...grpc.CallOption,
) error {
// リクエスト送信の事前処理
err := invoker(ctx, method, req, reply, cc, callOpts...) // 単項RPC処理
// リクエスト送信の事後処理
return err
}
}
ストリーミングRPCの場合¶
▼ 既製のインターセプター (StreamClientInterceptor
)¶
gRPCでは、ストリーミングRPCを送信するクライアント側のミドルウェア処理は、StreamClientInterceptor
という名前にすることが定められている。
type StreamClientInterceptor func(
ctx context.Context,
desc *StreamDesc,
cc *ClientConn,
method string,
streamer Streamer,
opts ...CallOption,
) (ClientStream, error)
この関数は、リクエストでエラーが起こった場合に、内部的にSetStatus
関数を実行する。
エラー時に、Spanステータスとエラーメッセージをスパンに設定してくれる。
これをgRPCサーバーとのコネクション作成時に、WithChainStreamInterceptor
関数に渡す。
package main
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
// pb.goファイルを読み込む。
pb "github.com/hiroki-hasegawa/foo/foo"
)
func main() {
...
// gRPCサーバーとのコネクションを作成する
conn, err := grpc.DialContext(
ctx,
":7777",
grpc.WithChainStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)
defer conn.Close()
// gRPCクライアントを作成する
client := pb.NewFooServiceClient(conn)
// goサーバーをリモートプロシージャーコールする。
response, err := client.SayHello(
context.Background(),
&pb.Message{Body: "Hello From Client!"},
)
...
}
▼ 自前のインターセプター¶
関数に定められた引数を定義すれば、インターセプターとして使用できる。
package intercetor
import (
"google.golang.org/grpc"
)
func StreamClientInterceptor(
ctx context.Context,
desc *StreamDesc,
cc *ClientConn,
method string,
streamer Streamer,
opts ...CallOption,
) (grpc.ClientStream, error) {
// 事前処理
streamer, err := streamer(ctx, desc, cc, method, callOpts...) // ストリーミングRPC処理
// 事後処理
return &wrapClientStream{streamer}, err
}
type wrapClientStream struct {
grpc.ClientStream
}
ユーザー定義のオプションを渡したい場合は、grpc.StreamClientInterceptor
型を返却する関数とする。
package intercetor
import (
"google.golang.org/grpc"
)
func StreamClientInterceptor(opts ...fooOption) grpc.StreamServerInterceptor {
// オプションを使用してパラメーターを作成する
return func(
ctx context.Context,
desc *StreamDesc,
cc *ClientConn,
method string,
streamer Streamer,
opts ...CallOption,
) (grpc.ClientStream, error) {
// 事前処理
streamer, err := streamer(ctx, desc, cc, method, callOpts...) // ストリーミングRPC処理
// 事後処理
return &wrapClientStream{streamer}, err
}
}
type wrapClientStream struct {
grpc.ClientStream
}
03. サーバー側のインターセプター¶
サーバー側のインターセプターとは¶
gRPCでは、ミドルウェア処理として、インターセプターをレスポンス処理の前後に挿入する。
インターセプターの種類¶
▼ 認証系¶
記入中...
▼ メトリクス系¶
記入中...
▼ 分散トレース系¶
記入中...
▼ リカバー系¶
gRPCの処理で起こったパニックを、Internal Server Error
として処理する。
package main
import (
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/recovery"
"google.golang.org/grpc"
...
)
func main() {
// gRPCサーバーを作成する。
grpcServer := grpc.NewServer(
// 単項RPCのサーバーインターセプター処理
grpc.ChainUnaryInterceptor(
// リカバー処理
grpc_recovery.UnaryServerInterceptor(...),
),
)
...
}
▼ フィルター系¶
分散トレースの作成を無視する場合を設定する。
package main
import (
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/recovery"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters"
"google.golang.org/grpc"
...
)
func main() {
// gRPCサーバーを作成する。
grpcServer := grpc.NewServer(
// 単項RPCのサーバーインターセプター処理
grpc.ChainUnaryInterceptor(
// ヘルスチェックへのリクエストを無視する
grpc_recovery.UnaryServerInterceptor(otelgrpc.WithInterceptorFilter(filters.HealthCheck())),
),
)
...
}
03-02. インターセプターの設定方法¶
単項RPCの場合¶
▼ 既製のインターセプター (UnaryServerInterceptor
)¶
gRPCでは、単項RPCを受信するサーバー側のミドルウェア処理は、UnaryServerInterceptor
という名前にすることが定められている。
type UnaryServerInterceptor func(
ctx context.Context,
req interface{},
info *UnaryServerInfo,
handler UnaryHandler,
) (resp interface{}, err error)
▼ 自前のインターセプター¶
関数に定められた引数を定義すれば、インターセプターとして使用できる。
package interceptor
import (
"context"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
func UnaryServerInterceptor(
ctx context.Context,
req interface{},
info *UnaryServerInfo,
handler UnaryHandler,
) (resp interface{}, err error) {
// 事前処理
resp, err := handler(ctx, req) // 単項RPC処理
// 事後処理
return err
}
ユーザー定義のオプションを渡したい場合は、grpc.UnaryServerInterceptor
型を返却する関数とする。
package interceptor
import (
"context"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
func UnaryServerInterceptor(opts ...fooOption) grpc.UnaryServerInterceptor {
// オプションを使用してパラメーターを作成する
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
// 事前処理
resp, err := handler(ctx, req) // 単項RPC処理
// 事後処理
return err
}
}
ストリーミングRPCの場合¶
▼ 既製のインターセプター (StreamServerInterceptor
)¶
gRPCでは、ストリーミングRPCを受信するサーバー側のミドルウェア処理はStreamServerInterceptor
という名前にすることが定められている。
type StreamServerInterceptor func(
srv interface{},
ss ServerStream,
info *StreamServerInfo,
handler StreamHandler,
) error
▼ 自前のインターセプター¶
関数に定められた引数を定義すれば、インターセプターとして使用できる。
package interceptor
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
func StreamServerInterceptor(
srv interface{},
ss ServerStream,
info *StreamServerInfo,
handler StreamHandler,
) error {
// 事前処理
err := handler(srv, wrapServerStream(ctx, ss, cfg)) // ストリーミングRPC処理
// 事後処理
return err
}
ユーザー定義のオプションを渡したい場合は、grpc.StreamServerInterceptor
型を返却する関数とする。
package interceptor
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
func StreamServerInterceptor(opts ...fooOption) grpc.StreamServerInterceptor {
// オプションを使用してパラメーターを作成する
return func(
srv interface{},
ss ServerStream,
info *StreamServerInfo,
handler StreamHandler,
) error {
// 事前処理
err := handler(srv, wrapServerStream(ctx, ss, cfg)) // ストリーミングRPC処理
// 事後処理
return err
}
}
04. サーバー側の実装例¶
gRPCサーバー (インターセプターがない場合)¶
gRPCサーバーを実装する。
package main
import (
"context"
"fmt"
"log"
"net"
// pb.goファイルを読み込む。
pb "github.com/hiroki-hasegawa/foo/foo"
"google.golang.org/grpc"
)
// goサーバー
type Server struct {
}
// Helloを返信する関数
func (s *Server) SayHello (ctx context.Context, in *pb.Message) (*Message, error) {
log.Printf("Received message body from client: %v", in.Body)
return &pb.Message{Body: "Hello From the Server!"}, nil
}
func main() {
// gRPCサーバーを作成する。
grpcServer := grpc.NewServer()
// pb.goファイルでコンパイルされた関数を使用して、goサーバーをgRPCサーバーとして登録する。
// goサーバーがリモートプロシージャーコールを受信できるようになる。
pb.RegisterFooServiceServer(grpcServer, &Server{})
// goサーバーで待ち受けるポート番号を設定する。
listenPort, _ := net.Listen("tcp", fmt.Sprintf(":%d", 9000))
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// gRPCサーバーとして、goサーバーで通信を受信する。
if err := grpcServer.Serve(listenPort); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
...
}
gRPCサーバー (インターセプターを使用する場合)¶
▼ インターセプターを使用する場合について¶
gRPCサーバーでは、リクエスト/レスポンスの送受信前のミドルウェア処理として、インターセプターを実行できる。
非Chain
関数であれば単一のインターセプター、一方でChain
関数であれば複数のインターセプターを渡せる。
執筆時点 (202309/16) で、パッケージのv1
は非推奨で、v2
が推奨である。
package main
import (
"fmt"
"log"
"net"
// pb.goファイルを読み込む。
pb "github.com/hiroki-hasegawa/foo/foo"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/auth"
grpc_logging "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/logging"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/recovery"
grpc_selector "github.com/grpc-ecosystem/go-grpc-middleware/interceptors/selector"
"github.com/grpc-ecosystem/go-grpc-middleware/interceptors"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
func main() {
// gRPCサーバーを作成する。
grpcServer := grpc.NewServer(
// 単項RPCのサーバーインターセプター処理
grpc.ChainUnaryInterceptor(
// 認証処理
grpc_selector.UnaryServerInterceptor(...),
// メトリクス処理
grpc_prometheus.UnaryServerInterceptor(...),
// ロギング処理
grpc_logging.UnaryServerInterceptor(...),
// 分散トレースのスパン作成処理
otelgrpc.UnaryServerInterceptor(...),
// リカバー処理
grpc_recovery.UnaryServerInterceptor(...),
),
// ストリーミングRPCのサーバーインターセプター処理
grpc.ChainStreamInterceptor(
otelgrpc.StreamServerInterceptor(...),
recovery.StreamServerInterceptor(...),
),
)
...
// goサーバーで待ち受けるポート番号を設定する。
listenPort, _ := net.Listen("tcp", fmt.Sprintf(":%d", 9000))
// gRPCサーバーとして、goサーバーで通信を受信する。
if err := grpcServer.Serve(listenPort); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
...
}
ヘルスチェックサーバー¶
grpc_health_v1
パッケージのRegisterHealthServer
関数を使用して、gRPCサーバーをヘルスチェックサーバーとして登録する。
package main
import (
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
...
// gRPCサーバーを作成する。
grpcServer := grpc.NewServer(
...
)
...
healthCheckServer := health.NewServer()
grpc_health_v1.RegisterHealthServer(grpcServer, healthCheckServer)
// goサーバーで待ち受けるポート番号を設定する。
listenPort, _ := net.Listen("tcp", fmt.Sprintf(":%d", 9000))
// gRPCサーバーとして、goサーバーで通信を受信する。
if err := grpcServer.Serve(listenPort); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
...
}
05. gRPCクライアント側の実装例¶
gRPCクライアントパッケージ¶
記入中...
gRPCクライアント¶
gRPCクライアント側では、gRPCサーバーとのコネクションを作成する必要がある。
package main
import (
"log"
"golang.org/x/net/context"
"google.golang.org/grpc"
// pb.goファイルを読み込む。
pb "github.com/hiroki-hasegawa/foo/foo"
)
func main() {
// gRPCサーバーとのコネクションを作成する
conn, err := grpc.DialContext(
ctx,
":7777",
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// gRPCクライアントを作成する
client := pb.NewFooServiceClient(conn)
// goサーバーをリモートプロシージャーコールする。
response, err := client.SayHello(
context.Background(),
&pb.Message{Body: "Hello From Client!"},
)
if err != nil {
log.Fatalf("Error when calling SayHello: %v", err)
}
// goサーバーからの返却を確認する。
log.Printf("Response from server: %v", response.Body)
}
gRPCクライアント (インターセプターを使用する場合)¶
▼ インターセプターを使用する場合について¶
gRPCクライアントでは、リクエスト/レスポンスの送受信前のミドルウェアとして、インターセプターを実行できる。
非Chain
関数であれば単一のインターセプター、一方でChain
関数であれば複数のインターセプターを渡せる。
▼ ストリーミングRPCの場合¶
package main
import (
"google.golang.org/grpc"
)
func main() {
...
// gRPCサーバーとのコネクションを作成する
conn, err := grpc.DialContext(
ctx,
":7777",
// ストリーミングRPCのインターセプター処理
grpc.WithChainStreamInterceptor(
myStreamClientInteceptor1,
myStreamClientInteceptor2,
),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
...
}
06. gRPCサーバーとクライアントの両方の実装例¶
proto
ファイル¶
クライアントからのコールで返信する構造体や関数を定義する。
// protoファイルの構文のバージョンを設定する。
syntax = "proto3";
import "google/api/annotations.proto";
// pb.goファイルでコンパイルされる時のパッケージ名
package foo;
// gRPCクライアント側からのリモートプロシージャーコール時に渡す引数を定義する。
// フィールドのタグを1としている。メッセージ内でユニークにする必要があり、フィールドが増えれば別の数字を割り当てる。
message Message {
string body = 1;
}
// 単項RPC
// gRPCクライアント側からリモートプロシージャーコールされる関数を定義する。
service FooService {
rpc SayHello(Message) returns (Message) {
// エンドポイント
option (google.api.http).get = "/foo";
}
}
- https://future-architect.github.io/articles/20220624a/#grpc-gateway%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E9%96%8B%E7%99%BA%E3%81%AE%E6%B5%81%E3%82%8C
- https://qiita.com/gold-kou/items/a1cc2be6045723e242eb#%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA%E3%81%A7%E9%AB%98%E9%80%9F%E5%8C%96
- https://christina04.hatenablog.com/entry/protoc-usage
pb.go
ファイル¶
事前に用意したproto
ファイルを使用して、pb.go
ファイルをコンパイルする。
pb.go
ファイルには、gRPCクライアントとgRPCサーバーの両方が参照するための構造体や関数が定義されており、ユーザーはこのファイルをそのまま使用すれば良い。
proto
コマンドを実行し、以下のようなpb.go
ファイルをコンパイルできる。
# foo.protoファイルから、gRPCに対応するfoo.pb.goファイルをコンパイルする。
$ protoc -I=. --go_out=. --go-grpc_out=. foo.proto
// コメントアウトに元になった.protoファイルの情報が記載されている
//
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.12
// source: foo.proto
// 〜 中略 〜
func RegisterFooServiceServer(s *grpc.Server, srv FooServiceServer) {
s.RegisterService(&_FooService_serviceDesc, srv)
}
// 〜 中略 〜
補足として、pb.go
ファイルには、gRPCサーバーとして登録するためのRegister<ファイル名>ServiceServer
関数が定義される。
07. メタデータ¶
メタデータの操作¶
▼ Append¶
送信/受信するgRPCリクエストのコンテキストにメタデータを設定する。
コンテキストにメタデータがすでにある場合は追加し、もしなければメタデータを新しく作成する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータを作成する
md := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
// メタデータにキーを設定する
md.Append("Baz", "baz")
...
}
▼ AppendToOutgoingContext¶
リクエスト送信用のコンテキストにメタデータとしてキーバリューを設定する。
コンテキストにメタデータがすでにある場合は追加し、もしなければメタデータを新しく作成する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータをコンテキストに設定する
ctx = metadata.AppendToOutgoingContext(ctx, "Foo", "foo")
...
}
▼ FromIncomingContext¶
受信したgRPCリクエストのコンテキストからメタデータを取得する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// コンテキストにメタデータがあれば取得する
md, ok := metadata.FromIncomingContext(ctx)
// メタデータがなければ作成する
if !ok {
md = metadata.MD{}
}
log.Print(md)
...
}
内部的にはmdIncomingKey
というコンテキストキー名を指定している。
このキー名は、NewIncomingContext
関数がメタデータ付きのコンテキスト作成時に設定する。
func ValueFromIncomingContext(ctx context.Context, key string) []string {
...
md, ok := ctx.Value(mdIncomingKey{}).(MD)
...
}
func NewIncomingContext(ctx context.Context, md MD) context.Context {
return context.WithValue(ctx, mdIncomingKey{}, md)
}
▼ FromOutgoingContext¶
送信するgRPCリクエストのコンテキストからメタデータを取得する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// コンテキストにメタデータがあれば取得する
md, ok := metadata.FromOutgoingContext(ctx)
// メタデータがなければ作成する
if !ok {
md = metadata.MD{}
}
...
}
内部的にはmdIncomingKey
というコンテキストキー名を指定している。
このキー名は、NewOutgoingContext
関数がメタデータ付きのコンテキスト作成時に設定する。
func ValueFromIncomingContext(ctx context.Context, key string) []string {
...
md, ok := ctx.Value(mdIncomingKey{}).(MD)
...
}
func NewOutgoingContext(ctx context.Context, md MD) context.Context {
return context.WithValue(ctx, mdOutgoingKey{}, rawMD{md: md})
}
▼ Get¶
送信/受信するgRPCリクエストのコンテキストからメタデータを取得する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータを作成する
md := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
// メタデータから値を取得する
val = md.Get("foo")
...
}
▼ New¶
メタデータを作成する。
『grpc-
』から始まるキー名はgRPCで予約されている。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータを作成する
md := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
...
}
空のメタデータを作成する場合は、MD
構造体を初期化しても良い。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータを作成する
md := metadata.MD{}
...
}
▼ NewIncomingContext¶
リクエスト受信用のコンテキストにメタデータを設定する。
コンテキストにメタデータがすでにある場合は置換するため、もしメタデータにキーを追加したい場合はAppendToOutgoingContext
関数を使用する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータを作成する
md := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
// メタデータをコンテキストに設定する
ctx = metadata.NewIncomingContext(ctx, md)
...
}
▼ NewOutgoingContext¶
リクエスト送信用のコンテキストにメタデータを設定する。
コンテキストにメタデータがすでにある場合は置換するため、もしメタデータにキーを追加したい場合はAppendToOutgoingContext
関数を使用する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータを作成する
md := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
// メタデータをコンテキストに設定する
ctx = metadata.NewOutgoingContext(ctx, md)
...
}
▼ Set¶
送信/受信するgRPCリクエストのコンテキストにメタデータを設定する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
...
// メタデータを作成する
md := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
// メタデータにキーを設定する
md.Set("Baz", "baz")
...
}
クライアントからサーバーに単項RPCを送信する場合¶
▼ クライアント側¶
クライアント側では、メタデータを設定する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
// メタデータを作成する
md := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
// メタデータをコンテキストに設定する
ctx = metadata.NewOutgoingContext(ctx, md)
...
}
▼ サーバー側¶
サーバー側では、メタデータを取得する。
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
// コンテキストにメタデータがあれば取得する
md, ok := metadata.FromIncomingContext(ctx)
// メタデータがなければ作成する
if !ok {
md = metadata.MD{}
}
log.Print(md)
...
}
サーバーからクライアントに単項RPCを送信する場合¶
▼ サーバー側¶
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
// メタデータを作成する
headMd := metadata.New(map[string]string{
"Foo": "foo",
"Bar": "bar",
})
// メタデータをヘッダーに設定する
if err := grpc.SetHeader(ctx, headerMD); err != nil {
return nil, err
}
// メタデータをトレーラーに設定する
if err := grpc.SetTrailer(ctx, trailerMD); err != nil {
return nil, err
}
...
}
▼ クライアント側¶
package main
import (
"google.golang.org/grpc/metadata"
)
func (s *fooServer) Foo(ctx context.Context, req *foopb.FooRequest) (*foopb.FooResponse, error) {
var header, trailer metadata.MD
// ヘッダーやトレーラーからメタデータを取得する
res, err := client.Hello(
ctx,
req,
grpc.Header(&header),
grpc.Trailer(&trailer),
)
if err != nil {
md = metadata.MD{}
}
log.Print(md)
...
}