BambTech

コンピュータサイエンスの学習記録等です。

gRPC で遊んでみた話

仕事で gRPC を本格的に使いそうかつ、昔から興味があったものの実際に手を動かしたことがなかったので、使ってみた。
基本的には 公式チュートリアル に合わせてやった。

gRPC とは

RPC を実現するために Google が開発した通信プロトコルの一つ。サーバー間での高速な通信を実現できるから、マイクロサービスとかで注目されている。
詳しくは gRPCって何? によくまとめられていたから、忘れた時には確認したい。
上記のところでまとめられていない内容として、通信の定義がある。

Simple RPC

クライアントから一つのリクエストがサーバー側へ送信され、一つのレスポンスを待つ状態。いわゆる普通の通信。
f:id:wamiota:20200516121354j:plain

Server-Side Streaming RPC

クライアントから一つのリクエストがサーバー側へ送信され、サーバーとしてはメッセージをシーケンスとして返すことができる。クライアントは、レスポンスをすべてのメッセージがなくなるまで読み取る。
f:id:wamiota:20200516121934j:plain

Client-Side Streaming RPC

サーバーの逆クライアントバージョン。クライアントはメッセージの送信を終えると、サーバー側が全てを読み取りレスポンスを返すまで待ち状態になる。
f:id:wamiota:20200516122124j:plain

Bidirectional Streaming RPC

bidirectional なので、双方向にストリーミングを可能としたもの。上記2つの組み合わせ的な感じ。read stream と write stream は独立で操作されるため、好きな順番で操作可能となっている。例えば、サーバーはレスポンスを書く前にすべてのメッセージを受け取ってもいいし、メッセージを受け取りながらレスポンスを変えしてもいいというもの。
f:id:wamiota:20200516123307j:plain

実際に実装してみた

環境

用意する環境としては、ローカルに作るのもなんか嫌だったから以下の docker-compose 内で走らせることにした。

# Dockerfile
FROM golang:1.13-stretch

SHELL ["/bin/bash", "-c"]
RUN apt update && apt-get install -y vim unzip

WORKDIR /protoc
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.12.0-rc2/protoc-3.12.0-rc-2-linux-x86_64.zip
RUN unzip protoc-3.12.0-rc-2-linux-x86_64.zip
RUN ln -s /protoc/bin/protoc /bin/protoc

WORKDIR /go-grpc
ENV GO111MODULE on
RUN go get -u github.com/golang/protobuf/protoc-gen-go
# docker-compose
version: "3.7"
services:
  go-grpc:
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: "go-grpc"
    volumes:
      - ./:/go-grpc
    tty: true
    privileged: true

これ以降、基本的な操作は protocol buffer を書く -> コンパイルする -> 実際に動かしてみるとなる。コマンドは以下の通り。

## compile
protoc --proto_path ./proto/hoge --go_out=plugins=grpc:./pb/fuga file_name.proto

動かしたもの

とりあえず simple rpc さえちゃんと理解できればあとは遊んでみるだけだから、simple rpc だけ備忘として書いておく。

// unary.proto
syntax = "proto3";

package unary;

// サービス名を定義し、リクエストタイプとレスポンスタイプを定義してあげる。
// この例では、リクエストが point でレスポンスが feature となる。
service RouteGuide {
    rpc GetFeature(Point) returns (Feature) {}
}

// リクエストの定義
message Point {
    int32 latitude = 1;
    int32 longitude = 2;
}

// レスポンスの定義
message Feature {
    string result = 1;
}

これをコンパイルすると、unary.pb.go ができるけど、ここの内容は割愛。
次に、サーバー側とクライアント側の準備をしてあげる。

// ./server/main.go

package main

import (
    "context"
    "fmt"
    "log"
    "net"

    pb "go-grpc/pb/unary"

    "github.com/pkg/errors"
    "google.golang.org/grpc"
)

const port = ":50051"

// ここで使用するサーバーの定義みたいな構造体を作る。
type routeGuideServer struct {
    pb.UnimplementedRouteGuideServer 
}

// 実際に、リクエストを取得して、レスポンスを返してあげるメソッド。
func (s *routeGuideServer) GetFeature(ctx context.Context, in *pb.Point) (*pb.Feature, error) {
        // リクエストを取得
    a := in.GetLatitude()
    b := in.GetLongitude()

    log.Printf("get %v and %v from client\n", a, b)

        // 今回は特に何かの処理をするわけではなく、ちゃんと受け取ったよと結果を作ってあげただけ。
    reply := fmt.Sprintf("return recieved two value: %v %v", a, b)
    return &pb.Feature{
        Result: reply,
    }, nil
}

// サーバーを構築する
func setServer() error {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        return errors.Wrap(err, "failed to build server")
    }
    s := grpc.NewServer()
    var server routeGuideServer
    pb.RegisterRouteGuideServer(s, &server)
    if err := s.Serve(lis); err != nil {
        return errors.Wrap(err, "failed to build server")
    }
    return nil
}

func main() {
    fmt.Println("server start...")
    if err := setServer(); err != nil {
        log.Fatalf("%v", err)
    }
}
// ./client/main.go

package main

import (
    "context"
    "log"
    "time"

    "github.com/pkg/errors"

    pb "go-grpc/pb/unary"

    "google.golang.org/grpc"
)

// 送信したいリクエストを作るところ
func request(client pb.RouteGuideClient, a int32, b int32) error {
    ctx, cancel := context.WithTimeout(
        context.Background(),
        time.Second,
    )
    defer cancel()
    testRequest := pb.Point{
        Latitude: a,
        Longitude: b,
    }
    reply, err := client.GetFeature(ctx, &testRequest)
    if err != nil {
        return errors.Wrap(err, "failed to get response")
    }
    log.Printf("get response\n %s", reply.GetResult())
    return nil
}

// クライアントのコネクションを構築してリクエストを最終的に送信しているところ。
func setClient(a int32, b int32) error {
    address := "localhost:50051"
    conn, err := grpc.Dial(
        address,
        grpc.WithInsecure(),
        grpc.WithBlock(),
    )
    if err != nil {
        return errors.Wrap(err, "failed to connect server")
    }
    defer conn.Close()
    client := pb.NewRouteGuideClient(conn)
    return request(client, a, b)
}

func main() {
    a := int32(10)
    b := int32(20)
    if err := setClient(a, b); err != nil {
        log.Fatalf("%v", err)
    }
}

とりあえず、ここまでは理解できたから、続いて他の通信方法についても遊んでみたいと思う。
ちなみに、実際の実行結果は以下の感じ

root@f5cd980e824f:/go-grpc# go run server/unary/main.go
server start...
2020/05/16 03:59:16 get 10 and 20 from client

root@f5cd980e824f:/go-grpc# go run client/unary/main.go
2020/05/16 03:59:16 get response
 return recieved two value: 10 20