みなさん、今年の2月末にGoogleが発表したgRPCをご存知でしょうか?gRPCとは、こちらも今年の5月にRFCとして公開されたばかりのHTTP/2を標準でサポートした新しいRPCフレームワークであり、効率的で拡張性の高いAPIや最近流行のマイクロサービスの作成をサポートします。本記事では、Goでのサンプルアプリケーションの作成を通してこのgRPCの通信を試してみようと思います。
gRPCの概要
図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でのみ動作確認しています。
アプリケーションの作成?実行までの流れは以下の通りです。
- grpc-goのダウンロード
- サービスの定義(protoファイルの作成)
- 2.で定義したサービスを満たすインターフェースの生成
- 3.で生成したインターフェースを満たすサーバーの実装
- 3.で生成したインターフェースを満たすクライアントの実装
- 実行
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ファイルは以下の通りです。
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply){}
}
message HelloRequest{
string name = 1;
}
message HelloReply {
string message = 1;
}
また、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) }
実行
では今回作成したプログラムをターミナルから実行してみましょう。
まずはサーバーを起動します。
次にクライアントを実行します。
サーバーから”Greeting: Hello, wdgk!”というレスポンスが返ってきていることが分かると思います。ちなみに、サーバーが起動していなかったり、指定するIPアドレス(ポート番号)を間違っていたりすると、
このように指定したサーバーへの接続に失敗します。
接続に成功すると、サーバー側ではこのようなログが吐かれています。
まとめ
今回はgRPCの基礎の基礎という形で、サンプルアプリケーションの作成を通じてgRPCの大体のイメージを掴んでもらいました。他にも異なる言語で実装した場合や、そもそも通信速度はどうなのか?等、未だ未検証なものはありますが、通信部分はgRPCに任せて開発者はロジック部分に専念する、というような切り分け方がスムーズに出来れば、gRPCの大きな強みの1つになりそうだなと思いました。また、マイクロサービス化が進められているサービスの中での各コンポーネント間の通信やhttp/2のメリットを享受できるサービス等から少しずつ普及していくのではと考えています。
本記事が皆様がgRPCを始める第1歩になれば幸いです。
※今回使用したソースコードは以下のレポジトリに置いてあります。
https://github.com/wdgk/grpc-sampler/