grpcurl をコードリーディング

こんにちは、しゅん（[@MxShun](https://twitter.com/MxShun)）です。
今年11月にサイバーエージェントに中途入社、AI事業本部オペレーションテクノロジーに仲間入りしました。

<宣伝>

👉 同じく本日投稿の、CyberAgent Advent Calendar 2022「[私がやった Amazon S3 コストカット全ステップ](https://developers.cyberagent.co.jp/blog/archives/38950/)」よければご覧ください！
👉 社内の方は、*#times_mxshun* よければ覗いてあげてください！

\</宣伝>

# grpcurl
gRPC 界隈では言わずと知れた、gRPC サーバに対して cURL できるコマンドラインツールです。

> grpcurl is a command-line tool that lets you interact with gRPC servers. It's basically curl for gRPC servers.

https://github.com/fullstorydev/grpcurl

# コードリーディング
バージョンは [v1.8.7](https://github.com/fullstorydev/grpcurl/releases/tag/v1.8.7) です。

### `main.grpcurl`
処理の冒頭は、コマンドラインツールの所謂バリデーション処理です。読み飛ばします。

最初に着目すべきは、`grpcurl.BlockingDial` を[呼び出している箇所](https://github.com/fullstorydev/grpcurl/blob/25c896aa59ffc36f7d12cf5d6c18e9c8f4421bfa/cmd/grpcurl/grpcurl.go#L460)です。つまり、ここで gRPC サーバとのコネクションを確立しています。

```go
cc, err := grpcurl.BlockingDial(ctx, network, target, creds, opts...)
```

```grpcurl/grpcurl.go
// BlockingDial is a helper method to dial the given address, using optional TLS credentials,
// and blocking until the returned connection is ready. If the given credentials are nil, the
// connection will be insecure (plain-text).
func BlockingDial(ctx context.Context, network, address string, creds credentials.TransportCredentials, opts ...grpc.DialOption) (*grpc.ClientConn, error)
```

少し戻って `ctx` を追うと、タイムアウトだけコンテキストに[詰めていました](https://github.com/fullstorydev/grpcurl/blob/25c896aa59ffc36f7d12cf5d6c18e9c8f4421bfa/cmd/grpcurl/grpcurl.go#L387)。

```go
ctx := context.Background()
// 中略
ctx, cancel = context.WithTimeout(ctx, timeout)
```

https://go.dev/blog/context

DescriptorSource は、ProtoSets もしくは ProtoFiles、リフレクションに対応しているのであれば gRPC サーバからの情報をもとに生成します。

```go
if len(protoset) > 0 {
    // 中略
    fileSource, err = grpcurl.DescriptorSourceFromProtoSets(protoset...)
    // 中略
} else if len(protoFiles) > 0 {
    // 中略
    fileSource, err = grpcurl.DescriptorSourceFromProtoFiles(importPaths, protoFiles...)
    // 中略
}
if reflection.val {
    md := grpcurl.MetadataFromHeaders(append(addlHeaders, reflHeaders...))
    refCtx := metadata.NewOutgoingContext(ctx, md)
    cc = dial()
    refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(cc))
    reflSource := grpcurl.DescriptorSourceFromServer(ctx, refClient)
    if fileSource != nil {
        descSource = compositeSource{reflSource, fileSource}
    } else {
        descSource = reflSource
    }
} else {
    descSource = fileSource
}
```

そして、`grpcurl.InvokeRPC` で[実際に RPC しています](https://github.com/fullstorydev/grpcurl/blob/25c896aa59ffc36f7d12cf5d6c18e9c8f4421bfa/cmd/grpcurl/grpcurl.go#L707)。
`cc` には DescriptorSource でこそっと `dial()` を代入しており、`ch` として確立されたチャネルが渡されています。

```go
err = grpcurl.InvokeRPC(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, rf.Next)
```

```grpcurl/invoke.go
// InvokeRPC uses the given gRPC channel to invoke the given method. The given descriptor source
// is used to determine the type of method and the type of request and response message. The given
// headers are sent as request metadata. Methods on the given event handler are called as the
// invocation proceeds.
//
// The given requestData function supplies the actual data to send. It should return io.EOF when
// there is no more request data. If the method being invoked is a unary or server-streaming RPC
// (e.g. exactly one request message) and there is no request data (e.g. the first invocation of
// the function returns io.EOF), then an empty request message is sent.
//
// If the requestData function and the given event handler coordinate or share any state, they should
// be thread-safe. This is because the requestData function may be called from a different goroutine
// than the one invoking event callbacks. (This only happens for bi-directional streaming RPCs, where
// one goroutine sends request messages and another consumes the response messages).
func InvokeRPC(ctx context.Context, source DescriptorSource, ch grpcdynamic.Channel, methodName string,
	headers []string, handler InvocationEventHandler, requestData RequestSupplier)
```

`grpcurl.InvokeRPC` に直接的な戻り値はないですが、EventHandler を通じてレスポンスやステータスコードが返却されるようです。そして、EventHandler に乗っかってきた値をもとにクライアントに結果を返却しています。

意外とシンプルな作りでした。

### `grpcurl.grpcurl`
`main.grpcurl` はコマンドラインツール機能を提供しますが、`grpcurl.grpcurl` は grpcurl のコア機能を提供しています。

```go
// Package grpcurl provides the core functionality exposed by the grpcurl command, for
// dynamically connecting to a server, using the reflection service to inspect the server,
// and invoking RPCs. The grpcurl command-line tool constructs a DescriptorSource, based
// on the command-line parameters, and supplies an InvocationEventHandler to supply request
// data (which can come from command-line args or the process's stdin) and to log the
// events (to the process's stdout).
```

API ドキュメント的に一部を抜粋してまとめてみます。

| 戻り値型 | メソッド | 説明 |
| --- | --- | --- |
| `[]string, error` | `ListServices(source DescriptorSource)` | DescriptorSource をもとに gRPC サービス一覧を取得します。 |
| `[]*desc.FileDescriptor, error` | `GetAllFiles(source DescriptorSource)` | DescriptorSource からファイルの descriptor 一覧を取得します。 |
| `int` | `(f filesByName) Len()` | `[]*desc.FileDescriptor` に対して `sort.Interface` を満たすための `Len` を提供します。 |
| `bool` | `(f filesByName) Less(i, j int)` |  `[]*desc.FileDescriptor` に対して `sort.Interface` を満たすための `Less` を提供します。 |
| - | `(f filesByName) Swap(i, j int)` |  `[]*desc.FileDescriptor` に対して `sort.Interface` を満たすための `Swap` を提供します。 |
| `[]string, error`| `ListMethods(source DescriptorSource, serviceName string)` | DescriptorSource とサービス名をもとに提供されるメソッド一覧を取得します。 |
| `metadata.MD` | `MetadataFromHeaders(headers []string)` | `"Header-Name: Header-Value"` というキーバリュー形式のヘッダーをメタデータに変換します。 |
| `[]string, error` | `ExpandHeaders(headers []string)` | ヘッダーに含まれる環境変数を展開します。 |
| `string` | `MetadataToString(md metadata.MD)` | メタデータを標準出力用テキストに変換します。 |
| `string, error` | `GetDescriptorText(dsc desc.Descriptor, _ DescriptorSource)` | Descriptor が提供するものをスニペットとしてテキスト形式で取得します。 |
| `proto.Message` | `EnsureExtensions(source DescriptorSource, msg proto.Message)` | 渡した `proto.Message` に対し DescriptorSource が提供する gRPC サーバ拡張機能を加味した動的メッセ―ジとして取得する。 |
| `error` | `fetchAllExtensions(source DescriptorSource, ext *dynamic.ExtensionRegistry, md *desc.MessageDescriptor, alreadyFetched map[string]bool)`| gRPC サーバ拡張機能一覧を `ext`　に取得します。 |
| `proto.Message` | `MakeTemplate(md *desc.MessageDescriptor)` | `MessageDescriptor` に適した JSON 形式の `proto.Message` を返却します。 |
| `credentials.TransportCredentials, error` | `ClientTransportCredentials(insecureSkipVerify bool, cacertFile, clientCertFile, clientKeyFile string)` | 与えられたプロパティをもとに TLS 設定を構築し、gRPC クライアントの`TransportCredentials` を生成・返却する。 |
| `*tls.Config, error` | `ClientTLSConfig(insecureSkipVerify bool, cacertFile, clientCertFile, clientKeyFile string)` | gRPC クライアントの TLS 設定を生成する。 |
| `credentials.TransportCredentials, error`| ` ServerTransportCredentials(cacertFile, serverCertFile, serverKeyFile string, requireClientCerts bool)` | 与えられたプロパティをもとに TLS 設定を構築し、gRPC サーバの`TransportCredentials` を生成・返却する。 |
| `*grpc.ClientConn, error` | `BlockingDial(ctx context.Context, network, address string, creds credentials.TransportCredentials, opts ...grpc.DialOption)` | TLS Credentials をもとに確率したコネクションを返却する。 |

# 所感
gRPC クライアントを知ることで、gRPC そのものの仕組みも再確認できました。

また興味本位でコードリーディングする機会があれば記事にしたいと思います。それではよいクリスマスを！