LoginSignup
4
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-05

はじめに

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のリポジトリから確認できます

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

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

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が入ったディレクトリ、サーバ側の出力ディレクトリ、クライアント側の出力ディレクトリを同期させます。

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パッケージを指定する場合、例えば

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でファイル内検索すると見つかりやすい)、その部分をみてメソッドを実装していきます。

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をつくります。ここで注意なのが、第3引数以降で可変長引数を受け取るようになっていますが、実装側のメソッドではoptsは書かなくていいです。

hoge_controller.go
type HogeController struct{
}

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

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

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の場合は以下のような関数で通信できます。

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方式の通信を使用して実装をしているところもあるので、後日その情報も共有していきたいです。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3