新しいRPCフレームワーク、gRPCをGoで試してみる。

ご注意

この記事は 2015年9月10日 に書かれたものです。内容が古い可能性がありますのでご注意ください。

  みなさん、今年の2月末にGoogleが発表したgRPCをご存知でしょうか?gRPCとは、こちらも今年の5月にRFCとして公開されたばかりのHTTP/2を標準でサポートした新しいRPCフレームワークであり、効率的で拡張性の高いAPIや最近流行のマイクロサービスの作成をサポートします。本記事では、Goでのサンプルアプリケーションの作成を通してこのgRPCの通信を試してみようと思います。

gRPCの概要

grpc_concept_diagram_00

図1.gRPCの全体像(http://www.grpc.io/docs/より転載)

 gRPCのサーバーとクライアントはお互いに様々な環境(Google内部のサーバーから、各自のデスクトップ環境まで)で通信でき、gRPCがサポートしている言語(C++, Java, Go, Python, Ruby, Node.js, Android Java, C#, Objective-C, PHP)で書くことができます。 例えばサーバーをJava,クライアントをGoやPython, Rubyで実装する、といった感じです。

サンプルアプリケーションを作ってみよう!

では早速ですが実際にGoからgRPCを試してみたいと思います。 今回はシンプルなメッセージのやり取りを行うだけのサーバーとクライアントをそれぞれ実装します。 また、以下のプログラムはMacOSX 10.10.5でのみ動作確認しています。 アプリケーションの作成〜実行までの流れは以下の通りです。
  1. grpc-goのダウンロード
  2. サービスの定義(protoファイルの作成)
  3. 2.で定義したサービスを満たすインターフェースの生成
  4. 3.で生成したインターフェースを満たすサーバーの実装
  5. 3.で生成したインターフェースを満たすクライアントの実装
  6. 実行
gRPC  

grpc-goのダウンロード

今回はGoでgRPC通信を試すため、まずgRPCのGo実装であるgrpc-goをインストールします。 以下のURLからダウンロードしてインストールしてください。 https://github.com/grpc/grpc-go

protoファイルの作成

次にprotoファイルを作成します。protoファイルの作成方法については以下のドキュメントを参照ください。 https://developers.google.com/protocol-buffers/docs/proto3?hl=ja 今回作成したprotoファイルは以下の通りです。 helloworld.proto
syntax = "proto3";

package helloworld;

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

message HelloRequest{
	string name = 1;
}

message HelloReply {
	string message = 1;
}
nameを受け取ってmessageを返すSayHello()を定義しています。 また、HelloRequestメッセージとHelloReplyメッセージはそれぞれnameとmessageというフィールドを持っています。各フィールドはタグ番号を持っていて、「=」の後で定義します。タグ番号は1以上の自然数で、(このタグ番号でフィールドを識別するため)重複なく必ず指定する必要があります。 次に定義したprotoファイルからサーバーとクライアントのインターフェースを生成します。このインターフェースはprotocプログラム(とGoの場合はそれに加えてprotocのGoプラグイン)でprotoファイルをコンパイルすることにより自動生成されますので、まずはprotocプログラムとプラグインをダウンロードします。 https://github.com/google/protobuf 上記レポジトリをcloneもしくはダウンロードしてから以下の各コマンドを実行し、インストールを完了させてください。
$ ./autogen.sh
$ ./configure
$ make
$ make check
$ make install 
次にprotoc-gen-goをインストールします。下記コマンドを実行し、protoc-gen-goをインストールを完了させてください。
$ go get github.com/golang/protobuf/protoc-gen-go
これでインターフェースの生成準備が出来ました。

インターフェースの生成

先ほど作成したprotoファイルと同ディレクトリ内で以下のコマンドを実行し、定義したprotoファイルに基づいたインターフェースを生成してください。
$ protoc --go_out=plugins=grpc;. helloworld.proto
実行すると、helloworld.pb.goというファイル名で先ほど作成したprotoファイルに応じた以下のようなファイルが作成されると思います。 ※このファイルを修正する必要はありません。
// Code generated by protoc-gen-go.
// source: helloworld.proto
// DO NOT EDIT!

/*
Package helloworld is a generated protocol buffer package.

It is generated from these files:
	helloworld.proto

It has these top-level messages:
	HelloRequest
	HelloReply
*/
package helloworld

import proto "github.com/golang/protobuf/proto"

import (
	context "golang.org/x/net/context"
	grpc "google.golang.org/grpc"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal

type HelloRequest struct {
	Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}

func (m *HelloRequest) Reset()         { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage()    {}

type HelloReply struct {
	Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}

func (m *HelloReply) Reset()         { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage()    {}

// Client API for Greeter service

type GreeterClient interface {
	SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type greeterClient struct {
	cc *grpc.ClientConn
}

func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
	return &greeterClient{cc}
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
	out := new(HelloReply)
	err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// Server API for Greeter service

type GreeterServer interface {
	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
	s.RegisterService(&_Greeter_serviceDesc, srv)
}

func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, codec grpc.Codec, buf []byte) (interface{}, error) {
	in := new(HelloRequest)
	if err := codec.Unmarshal(buf, in); err != nil {
		return nil, err
	}
	out, err := srv.(GreeterServer).SayHello(ctx, in)
	if err != nil {
		return nil, err
	}
	return out, nil
}

var _Greeter_serviceDesc = grpc.ServiceDesc{
	ServiceName: "helloworld.Greeter",
	HandlerType: (*GreeterServer)(nil),
	Methods: []grpc.MethodDesc{
		{
			MethodName: "SayHello",
			Handler:    _Greeter_SayHello_Handler,
		},
	},
	Streams: []grpc.StreamDesc{},
}
次に定義したサービスに基づいてサーバーとクライアントを作成します。

gRPCサーバーの実装

今回は以下のような受けたリクエスト(Name)に応じてレスポンス(Message)を返すという、シンプルな構成のサーバーをGoで作成しました。
package main

import (
	"flag"
	pb "github.com/wdgk/grpc-sampler/proto"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"log"
	"net"
)

var (
	addrFlag = flag.String("addr", ":5000", "Address host:post")
)

type server struct{}

//リクセスト(Name)を受け取り、レスポンス(Message)を返す
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("New Request: %v", in.String())
	return &pb.HelloReply{Message: "Hello, " + in.Name + "!"}, nil
}

func main() {
      //requestを受け付けるportを指定する
	lis, err := net.Listen("tcp", *addrFlag)

	if err != nil {
		log.Fatalf("boo")
	}

        //新しいgRPCサーバーのインスタンスを作成
	s := grpc.NewServer()
        //gRPCサーバーを保存する
	pb.RegisterGreeterServer(s, &server{})
	s.Serve(lis)
}

gRPCクライアントの実装

それに対し、以下のようなクライアントをGoで作成しました。
package main

import (
	"flag"
	pb "github.com/wdgk/grpc-sampler/proto"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"log"
)

var (
	addrFlag = flag.String("addr", "localhost:5000", "server address host:post")
)

func main() {
        //IPアドレス(ここではlocalhost)とポート番号(ここでは5000)を指定して、サーバーと接続する
	conn, err := grpc.Dial(*addrFlag)

	if err != nil {
	  log.Fatalf("Connection error: %v", err)
	}

           //接続は最後に必ず閉じる
	defer conn.Close()

	c := pb.NewGreeterClient(conn)

            //サーバーに対してリクエストを送信する
	resp, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "wdgk"})
	if err != nil {
		log.Fatalf("RPC error: %v", err)
	}
	log.Printf("Greeting: %s", resp.Message)
}

実行

では今回作成したプログラムをターミナルから実行してみましょう。 まずはサーバーを起動します。 スクリーンショット 2015-08-24 17.21.23 次にクライアントを実行します。 スクリーンショット 2015-08-24 17.24.11 サーバーから”Greeting: Hello, wdgk!”というレスポンスが返ってきていることが分かると思います。ちなみに、サーバーが起動していなかったり、指定するIPアドレス(ポート番号)を間違っていたりすると、 スクリーンショット 2015-09-07 12.04.34 スクリーンショット 2015-09-09 09.51.40 このように指定したサーバーへの接続に失敗します。 接続に成功すると、サーバー側ではこのようなログが吐かれています。 スクリーンショット 2015-08-24 17.27.08

まとめ

 今回はgRPCの基礎の基礎という形で、サンプルアプリケーションの作成を通じてgRPCの大体のイメージを掴んでもらいました。他にも異なる言語で実装した場合や、そもそも通信速度はどうなのか?等、未だ未検証なものはありますが、通信部分はgRPCに任せて開発者はロジック部分に専念する、というような切り分け方がスムーズに出来れば、gRPCの大きな強みの1つになりそうだなと思いました。また、マイクロサービス化が進められているサービスの中での各コンポーネント間の通信やhttp/2のメリットを享受できるサービス等から少しずつ普及していくのではと考えています。 本記事が皆様がgRPCを始める第1歩になれば幸いです。 ※今回使用したソースコードは以下のレポジトリに置いてあります。 https://github.com/wdgk/grpc-sampler/

Google のクラウドサービスについてもっと詳しく知りたい、直接話が聞いてみたいという方のために、クラウドエースでは無料相談会を実施しております。お申し込みは下記ボタンより承っておりますので、この機会にぜひ弊社をご利用いただければと思います。

無料相談会のお申込みはこちら