はじめに
Go製のAPIサーバとDartクライアント(Flutterを想定)間の通信をgRPCで行う方法について書きます
日比谷音楽祭おさんぽアプリ2020 開発の裏側を語る / サーバー編というDeNAの20新卒、21卒内定者の方の記事を参考にしたので共有しておきます。
また、なぜgRPCを使うと嬉しいのかは、Cloud Native Days Tokyo 2020の南 直さんのセッション "Real World Migration from HTTP to gRPC"を聞くとわかりやすいのでこちらも共有しておきます。リンクからアーカイブ動画に飛べます。
準備
.protoファイルから自動生成するにはprotocコマンドと周辺プラグインを使用する必要がありますがローカルにいろいろインストールするのは嫌なので、Dockerコンテナを立ち上げて各言語のコードを生成します。現在のProtocolBuffersの最新バージョンはGitHubのリポジトリから確認できます
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
このコンテナ内で以下のスクリプトを実行します
#!/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が入ったディレクトリ、サーバ側の出力ディレクトリ、クライアント側の出力ディレクトリを同期させます。
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パッケージを指定する場合、例えば
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でファイル内検索すると見つかりやすい)、その部分をみてメソッドを実装していきます。
// 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をつくります。ここで注意なのが、第3引数以降で可変長引数を受け取るようになっていますが、実装側のメソッドではoptsは書かなくていいです。
type HogeController struct{
}
func (ctrl HogeController) CreateHoge(ctx context.Context, in *CreateHogeMessage) (*CreateHogeResponse, error){
// TODO: return *CreateHogeResponse, error
}
上で作ったHogeControllerのインスタンスをRegisterHogeServer()でgRPCサーバインスタンスに登録します。
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の場合は以下のような関数で通信できます。
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方式の通信を使用して実装をしているところもあるので、後日その情報も共有していきたいです。