GoサーバとDartクライアントではじめるgRPC

# はじめに
Go製のAPIサーバとDartクライアント（Flutterを想定）間の通信をgRPCで行う方法について書きます

[日比谷音楽祭おさんぽアプリ2020 開発の裏側を語る / サーバー編](https://engineer.dena.com/posts/2020.06/2020-hibiya-festival-server/)というDeNAの20新卒、21卒内定者の方の記事を参考にしたので共有しておきます。

また、なぜgRPCを使うと嬉しいのかは、Cloud Native Days Tokyo 2020の南 直さんのセッション ["Real World Migration from HTTP to gRPC"](https://event.cloudnativedays.jp/cndt2020/talks/14)を聞くとわかりやすいのでこちらも共有しておきます。リンクからアーカイブ動画に飛べます。

# 準備
.protoファイルから自動生成するにはprotocコマンドと周辺プラグインを使用する必要がありますがローカルにいろいろインストールするのは嫌なので、Dockerコンテナを立ち上げて各言語のコードを生成します。現在のProtocolBuffersの最新バージョンは[GitHubのリポジトリ](https://github.com/protocolbuffers/protobuf/releases)から確認できます


```dockerfile:Dockerfile

FROM golang:1.15.0

ENV DEBIAN_FRONTEND=noninteractive

ARG PROTO_VERSION=3.13.0

WORKDIR /proto

COPY ./proto .

RUN mkdir /output /output/server /output/client

RUN apt-get -qq update && apt-get -qq install -y \
  unzip

RUN curl -sSL https://github.com/protocolbuffers/protobuf/releases/download/v${PROTO_VERSION}/protoc-${PROTO_VERSION}-linux-x86_64.zip -o protoc.zip && \
  unzip -qq protoc.zip && \
  cp ./bin/protoc /usr/local/bin/protoc && \
  cp -r ./include /usr/local

# Go
RUN go get -u github.com/golang/protobuf/protoc-gen-go

# Dart
RUN apt-get install apt-transport-https
RUN sh -c 'curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -'
RUN sh -c 'curl https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list'
RUN apt-get update
RUN apt-get install dart -y
ENV PATH="${PATH}:/usr/lib/dart/bin/"
ENV PATH="${PATH}:/root/.pub-cache/bin"
RUN pub global activate protoc_plugin
```

このコンテナ内で以下のスクリプトを実行します

```shell:protoc.sh
#!/bin/sh

set -xe

SERVER_OUTPUT_DIR=/output/server
CLIENT_OUTPUT_DIR=/output/client

protoc --version
protoc -I=/proto/protos hoge.proto fuga.proto\
  --go_out=plugins="grpc:${SERVER_OUTPUT_DIR}" \
  --dart_out="grpc:${CLIENT_OUTPUT_DIR}"

# protoファイル内でtimestamp.protoなどをimportしたときに必要
protoc -I=/proto/protos timestamp.proto wrappers.proto\
  --dart_out="grpc:${CLIENT_OUTPUT_DIR}"

```

docker-composeを使用する際はcommandで、先ほどのprotoc.shを実行するように設定しておけば立ち上げ時に自動でコードが生成されるようになります。volumesでprotoファイルとprotoc.shが入ったディレクトリ、サーバ側の出力ディレクトリ、クライアント側の出力ディレクトリを同期させます。

```yaml:docker-compose.yml
version: '3.8'

services:
  proto:
    build:
      context: .
      dockerfile: docker/proto/Dockerfile
    command: ./protoc.sh
    volumes:
      - ./proto:/proto
      - ./client:/output/client
      - ./server:/output/server
```

ただし、protoファイル側でgoパッケージを指定する場合、例えば

```proto:hoge.proto
syntax = "proto3";

package hoge.hoge;

option go_package = "hoge/fuga/foo/bar";

service Hoge{
}

```
だとすると、コンテナ側の```/output/server/hoge/fuga/foo```に出力されます。

# サーバサイド（Go）
docker-compose.ymlで指定したvolumesに```hoge.pb.go```が生成されます。その中にClinetのInerface定義があるので（ctxでファイル内検索すると見つかりやすい）、その部分をみてメソッドを実装していきます。

```go:hoge.pb.go
// HogeClient is the client API for Hoge service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type HogeClient interface {
	CreateHoge(ctx context.Context, in *CreateHogeMessage, opts ...grpc.CallOption) (*CreateHogeResponse, error)
}
```

HogeClientを実装するクラスとしてHogeControllerをつくります。ここで注意なのが、第３引数以降で可変長引数を受け取るようになっていますが、実装側のメソッドではoptsは書かなくていいです。


```go:hoge_controller.go
type HogeController struct{
}

func (ctrl HogeController) CreateHoge(ctx context.Context, in *CreateHogeMessage) (*CreateHogeResponse, error){
	// TODO: return *CreateHogeResponse, error
}
```

上で作ったHogeControllerのインスタンスをRegisterHogeServer()でgRPCサーバインスタンスに登録します。


```go:main.go
func main() {
	listenPort, err := net.Listen("tcp", ":8000")
	if err != nil {
		log.Fatalln(err)
	}
	server := grpc.NewServer()

	hogeCtrl := NewHogeController()
	pb.RegisterHogeServer(server, &hogeCtrl)

	if err := server.Serve(listenPort); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
```

# クライアントサイド（Dart）
クライアントは```hoge.pb.dart```, ```hoge.pbgrpc.dart```, ```hoge.pbjson.dart```, enumを使っている場合は```hoge.pbenum.dart```が生成されます。

protoファイル側でtimestamp.dartを使用した場合は生成されたパスではエラーになるので単純に```hoge.pb.dart```内のimport文を```import 'timestamp.pb.dart' as $1;```のように変更すると使用できます。Unary Callの場合は以下のような関数で通信できます。

```dart:hoge.dart
Future<void> createHoge(dartSideParam1, dartSideParam2, dartSideParam3) async {
    final channel = ClientChannel('localhost',
        port: 8000,
        options:
            const ChannelOptions(credentials: ChannelCredentials.insecure()));
    final grpcClient = KitchenClient(channel,
        options: CallOptions(timeout: Duration(seconds: 10)));
    try {
      await grpcClient.createHoge(CreateHogerMessage()
        ..param1 = dartSideParam1
        ..param2 = dartSideParam2
        ..param3 = dartSideParam3;
      await channel.shutdown();
    } catch (error) {
      developer.log('Caught error: $error');
      await channel.shutdown();
      return Future.error(error);
    }
}

```

# まとめ
gRPCを使用してGoとDart間を通信する方法でした。業務ではServer Streaming方式の通信を使用して実装をしているところもあるので、後日その情報も共有していきたいです。