Hello, gRPC!!


はじめに

この記事はユニークビジョン株式会社 Advent Calendar 2018の11日目の記事です。

コンテナを中心とした開発が主流になっていくなかで、コンテナ間の通信はどうするのかという問題がつきまといます。
普通にAPIとして実装しても良いと思いますが、Googleが開発しているgRPCを活用すれば通信方法やAPIのドキュメントの準備の手間を軽減したり、HTTP/2を利用した良好なパフォーマンスなど様々なメリットを享受できます。

社内への宣伝を兼ねて、今回gRPCのチュートリアルをやってみたいと思います。

gRPC

Google社が開発しているRPCフレームワークです。.protoを定義することで、そこから様々な言語のコードを生成し、サーバにメソッドを定義することで、異なる言語間で通信が可能となります。
公式のドキュメントに対応言語のチュートリアルなどがまとまっており、今回はここ1を参考にしつつ実装していきます。

Hello, World

準備

 環境

  • Go(今回は1.10を使用します)
    • Goのセットアップに関してはGo公式のドキュメントやその他の記事を参照してください
    • $GOPATH定義までしてあるといいとおもいます

ライブラリ

gRPCと併せて今回goで実装するのでGoPluginも

$ go get -u google.golang.org/grpc
$ go get -u github.com/golang/protobuf/protoc-gen-go

ディレクトリ構成

適当なディレクトリ($GOPATH以下が望ましい)に以下のような形で準備します。

.
├── client
│   └── main.go
├── hellogrpc
│   └── hellogrpc.proto
└── server
    └── main.go

.protoの定義

今回はhellogrpcディレクトリ以下に、サーバ - クライアント間のインタフェースを定義するprotoファイルを設置します。

このprotoファイルを元にgRPCがサポートしている各言語の実装を生成します。

hellogrpc.proto
syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.tokikokoko.hellogrpc.server";
option java_outer_classname = "HelloGrpc";

package HelloGrpc;

// The greeting service definition.
service HelloGrpc {
    // Sends a greeting
    rpc GreetServer (GreetRequest) returns (GreetMessage) {}
}

message GreetRequest {
    string name = 1;
}

message GreetMessage {
    string msg = 1;
}

上記のファイルで注目してほしいのはpackage HelloGrpc;以下で、

// The greeting service definition.
service HelloGrpc {
    // Sends a greeting
    rpc GreetServer (GreetRequest) returns (GreetMessage) {}
}

service内で今回実装するgRPCサーバにどのようなメソッドを実装するのかを定義します。

rpcに続く文がserviceのメソッドとなり、ここではGreetServerというメソッドを定義しています。これはGreetRequest型のmessageを引数としてGreetMessage型のmessageを返すという定義です。

message GreetRequest {
    string name = 1;
}

message GreetMessage {
    string msg = 1;
}

messageに続く文でメッセージ型を定義しており、構造体のように扱えます。

ここでは

  • string型のnameを持ったGreetRequest
  • string型のmsgを持ったGreetMessage

を定義しています。

.protoをコンパイル

定義した.protoファイルはコンパイルすることで、各言語の実装を出力します。出力されたコードをimportすることで定義したインタフェースを実装、呼び出すことができます。

今回定義したprotoファイルで、Go言語のコードを出力するためには、

$ protoc -I hellogrpc/ hellogrpc/hellogrpc.proto --go_out=plugins=grpc:hellogrpc

を実行することで、hellogrpc/hellogrpc.pb.goが出力されます。

サーバとクライアントを実装する

出力されたコードをimportして、実際にサーバとクライアントを実装していきます。

server/main.go
package main

import (
    "context"
    "fmt"
    "net"

    "google.golang.org/grpc"
    "log"

    // .protoから生成されたコードをimportしている
    // 今回は筆者の$GOPATH内に作成したので適宜プロジェクトを作成したパスに合わせる
    pb "github.com/tokikokoko/hello_grpc/hellogrpc"
)

// gRPC server struct
type server struct {
}

// .protoで定義したGreetServerを定義している
func (s *server) GreetServer(ctx context.Context, p *pb.GreetRequest) (*pb.GreetMessage, error) {
    log.Printf("Request from: %s", p.Name)
    return &pb.GreetMessage{Msg: fmt.Sprintf("Hello, %s. ", p.Name)}, nil
}

func main() {
    // gRPC
    port := 10000
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    log.Printf("Run server port: %d", port)
    grpcServer := grpc.NewServer()
    // メソッドを定義
    pb.RegisterHelloGrpcServer(grpcServer, &server{})
    // gRPCサーバを公開
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
client/main.go
package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    // .protoから生成されたコードをimportしている
    // 今回は筆者の$GOPATH内に作成したので適宜プロジェクトを作成したパスに合わせる
    pb "github.com/tokikokoko/hello_grpc/hellogrpc"
)

const (
    address = "localhost:10000"
)

func main() {
    // gRPCサーバへのconnectionを作成
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    // connection終了処理
    // 関数終了後に実行される
    defer conn.Close()

    c := pb.NewHelloGrpcClient(conn)

    name := "ほげ太郎"

    // タイムアウトを設定
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    // .protoで定義したサーバのGreetServerを呼び出している
    r, err := c.GreetServer(ctx, &pb.GreetRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Response: %s", r.Msg)
}

実行結果

サーバ側
$ go run server/main.go
2018/12/11 00:37:24 Run server port: 10000
2018/12/11 00:37:26 Request from: ほげ太郎
クライアント側
$ go run client/main.go
2018/12/11 00:37:26 Response: Hello, ほげ太郎.

まとめ

超簡単なgRPCサーバ&クライアントを実装しました。gRPCの概要を理解してもらえれば幸いです。超ぬるい記事ですが、初投稿なのでお許しくださいorz

明日も私が担当です。CI/CDするとか言っていましたがgRPCの導入の話が存外に長くなってしまったので、今日に引き続きgRPCのstreamを使った簡素なチャットツールを実装しようと思います。