Go言語でgRPCを簡単なgRPCサーバを立てる

Blog Single

gRPCとは

Googleが2015年に発表したHTTP/2を使ったRPC(Remote Procedure Call, 遠隔手続き呼出し)フレームワークです。

Protocol Bufferでデータをシリアライズをすることによってデータサイズを小さくすることによって通信料を減らしたり、インターフェースの定義ファイル(.proto)から様々な言語にコードを生成することができるため、静的型付言語などでJSONに対応する型を書く手間が省けたり、型をクライアント側に強制することができます。

gRPCは、バックエンド間の通信などに使われ、最近は大きなシステムの開発にマイクロサービスアーキテクチャ(大きなシステムのスケーラビリティの向上のために複数の小さいサービスに分割して連携することで機能を実装を行うアーキテクチャ)が採用されることが増えており、それに伴ってよりgRPCが重宝されています。

環境

$ go version
go version go1.10.4 darwin/amd64

gRPCのコード生成にprotocコマンドを用いるため、インストール

$ go get -u github.com/golang/protobuf/protoc-gen-go
$ protoc --version
libprotoc 3.6.0

.protoファイルの作成

Protocol BuffersのIDL(インターフェース定義言語)でgRPCのインターフェースの定義を行います。

このファイルによって、様々な言語のソースコードの雛形を生成することができ、通信するデータのパースが簡単に行えます。

以下のようなファイルを定義します。

sample.proto

syntax = "proto3";

service Sample {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

service :サービス定義

今回はSayHelloという、受け取った引数にHelloという文字列を追加した文字列を返すメソッドを作るので、そのインターフェースの定義をします。

message : 引数や出力の型の宣言

作成した定義ファイルからGo言語のコード生成を行います。

生成するコードを格納するディレクトリの生成

$ mkdir pb
$ mkdir pb/sample
$ protoc --go_out=plugins=grpc:pb/sample sample.proto

これによって、sample.pg.goというファイルが生成され、構造体やインターフェースが定義されたファイルが生成されます。

構造体

type HelloRequest struct {
    Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

インターフェース

// SampleServer is the server API for Sample service.
type SampleServer interface {
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

実装

先ほど作成したインターフェースの実際の処理を行うサーバー側とそのgRPCのメソッドを呼び出すクライアントのコードの実装します。

公式が用意しているサンプルコードを参考にして作成します。

ファイル構成

.
├── client
│   └── main.go
├── pb
│   └── sample
│       └── sample.pb.go
├── sample.proto
└── server
    └── main.go

サーバー側のコード

サーバー側はインターフェースで定義したメソッドの処理を実際に行うコードを作成します。

package main

import (
    "context"
    "log"
    "net"

    pb "../pb/sample"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

const (
    port = ":50051"
)

type server struct{}

// 作成したインターフェースの実装
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello, " + in.Name + "!!"}, nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterSampleServer(s, &server{})
    // Register reflection service on gRPC server.
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

先ほど.protoファイルから生成したインターフェースのSayHelloを実装して、net.Listen("tcp", port) の箇所でtcpポートを開いてサーバーを立ち上げています。

クライアント側のコード

クライアント側は、先ほど立ち上げたサーバーに接続して、SayHelloを呼び出します。

package main

import (
    "context"
    "log"
    "os"
    "time"

    pb "../pb/sample"
    "google.golang.org/grpc"
)

const (
    address     = "localhost:50051"
    defaultName = "Taro"
)

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewSampleClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}

grpc.Dialの箇所で先ほど立ち上げたgRPCサーバーに接続を行い、.protoファイルから生成されたコードのメソッドに、宣言した型を渡すことによって先ほど処理を追加したSayHelloを実行します。

実行結果

サーバー側をgo run main.goで実行した状態で、クライアント側のコードを実行します。

$ go run main.go
2018/11/22 19:48:45 Greeting: Hello, Taro!!

まとめ

サンプルコードのなぞり書きで、深いところまでは全然理解できていませんが、型を異なるプログラミング言語のクライアント側とサーバー側で共有できるというのはなかなか便利そうでした。
サーバー間の連携を行うためのAPIを作る際、REST APIでは設計や型、JSONをパースするためにコードを書くのがキツく感じてきた際などは、かなり使えそうな感じがしました。

参考

Posted by MatsumotoKazuki
PHPやJavaで開発を行っているエンジニア。 LOLというゲームの試合を観戦するのが好きです。

Other Posts: