APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

GolangでgRPCを試してみる

はじめに

先進サービス開発事業部の山岡です。

仕事で使う機会がありそうなのでgRPC *1 に入門しました。まずは定番のHello, Worldからやってみたのでその内容を共有したいと思います。

用語解説

gRPCとは?

Googleが開発したRPCのフレームワークです。特徴としては異なる環境でもインターフェースを合わせるためにProtocol Buffers(Protobuf) *2 からサーバーとクライアントのコードを自動生成すること、双方向のストリーム通信に柔軟に対応できること、また他にもタイムアウトやキャンセル等RPCで必要になりそうな仕組みが一通り提供されます。

gRPCはいくつかの技術から成り立っており、HTTP/2を下地としてシリアライズには標準ではProtobufを使います。「標準では」と書いた通りそれ以外のシリアライズにも対応しており例えばJSONとして扱うことも可能です。やや脱線しますがGoでこれをやりたい場合はjsonpb *3 というパッケージが使えるようです(今回は試していない)。

基本的な使い方としてはProtobufでスキーマを定義し、protocコマンドで各言語(今回はGo)のコードを生成しその後処理内容を実装していく、という流れになります。

Protocol Buffers

これもGoogleによって開発されたシリアライズフォーマットで、バイナリーフォーマットを採用することで小さく・高速に通信できるのが特徴です。特に公式サイトではXMLとの対比を強調しています *4 。またIDLで定義したインターフェースに沿って各言語向けにコードを自動生成する仕組みとすることで汎用性が高められています(生成機能はプラグインで拡張できる)。

実際に使ってみるとすぐわかりますが、RESTful+JSONと違って通信をcurl等で手軽に見ることができないのはやや面倒かもしれません。

準備

然程難しい手順は必要無く、gRPCライブラリとProtobuf用コンパイラーのインストールだけなので以下のコマンドを叩いて下さい。

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

インターフェース定義

まずはProtobufでサービスを定義しましょう。今回は単純なハロワなのでリクエスト時には何もパラメーターを付けず、レスポンスにメッセージを付ける形にします。以下のようにファイルを作成し hello.proto というファイルネームで保存して下さい。

ざっくりと内容を説明するとHelloというサービスを定義しそこにGreetingというRPCを含めています。Greetingはリクエスト時にはサーバーへ何も送らず、レスポンスにメッセージ(文字列)が含まれます。

syntax = "proto3";

service Hello {
    rpc Greeting (SayHelloMessage) returns (SayHelloResponse) {}
}

message SayHelloMessage {
}

message SayHelloResponse {
    string msg = 1;
}

Goのコード生成

では先程書いたprotoファイルからGoのコードを生成してみましょう。出力先を別けるためhelloというディレクトリも用意します。

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

これで hello.pb.proto が生成されたはずです。中身は色々ややこしいので追わないことにしますが、ポイントになるのはRPCと同名のInterfaceです。これが既に定義されているので、具体的に処理を行うためにこのInterfaceを実装します。

実装

以下、 $GOPATH/src/local/grpc の中で作業していることを前提とします。

サーバー

まずはサーバーからいきましょう。やるべきことは主にRPCの実装(所定のテキストをレスポンスとして返却すること)とリッスンの開始です。

適当な空structを定義しそれに対し Greeting(ctx context.Context, message *hello.SayHelloMessage) (*hello.SayHelloResponse, error) のメソッドを実装します。今回は単なるハロワなのでレスポンス用の構造体メンバー hello.SayHelloResponse.Msg にstringのメッセージを入れて返却するだけです。

main.go

package main

import (
    "context"
    "log"
    "net"

    "local/grpc/hello"

    "google.golang.org/grpc"
)

type helloService struct{}

func (s helloService) Greeting(ctx context.Context, message *hello.SayHelloMessage) (*hello.SayHelloResponse, error) {
    return &hello.SayHelloResponse{
        Msg: "Hello, gRPC!",
    }, nil
}

func main() {
    port, err := net.Listen("tcp", ":25252")
    if err != nil {
        log.Println(err.Error())
        return
    }

    s := grpc.NewServer()
    hello.RegisterHelloServer(s, &helloService{})
    s.Serve(port)
}

クライアント

次にクライアントです。クライアントはリッスンしているサーバーに対しリクエストを投げ受け取ったメッセージを表示します。

これも go run で直接実行したいのですがmain()を同じディレクトリに用意することはできないので mkdir client でディレクトリを作成し1階層下のclientパッケージとしましょう。

Interface定義の都合上、空のリクエストでもパラメーターの入れ物であるstructをリクエスト用のメソッドに渡す必要があることに注意して下さい。今回のように何も渡さない場合は単純に空structのポインター &hello.SayHelloMessage{} を入れればOKです。ちなみに面倒くさがってnilを入れると実行時に 2019/07/31 13:24:19 rpc error: code = Internal desc = grpc: error while marshaling: proto: Marshal called with nil のようなエラーが起きます。

client/main.go

package main

import (
    "context"
    "fmt"
    "log"

    "local/grpc/hello"

    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("127.0.0.1:25252", grpc.WithInsecure())
    if err != nil {
        log.Println(err.Error())
        return
    }
    defer conn.Close()

    resp, err := hello.NewHelloClient(conn).Greeting(context.Background(), &hello.SayHelloMessage{})
    if err != nil {
        log.Println(err.Error())
        return
    }
    fmt.Printf("%s\n", resp.Msg)
}

実行

では実行しましょう。ターミナルを2枚開き go run でサーバーを起動後、クライアントを起動します。

  • サーバー
$ go run main.go
  • クライアント
$ go run client/main.go 
Hello, gRPC!

以上、無事にハロワの実装ができました。