grpc/grpc · GitHub
gRPC - An RPC library and framework
https://github.com/grpc/grpc
gRPC は Google が開発しているRPC(リモートプロシージャコール)のライブラリとフレームワークで、通信層は HTTP/2 を介して行われます。
データ層については、固定されている訳ではなくあくまでデフォルトで Protocol Buffers が使われる様になっています。使用出来るプログラミング言語は現在、C++, Node.js, Python, Ruby, Objective-C, PHP, C# となっています。
実はこれら以外にも grpc-go という、なぜかこのリストに加えられていないオフィシャルリポジトリがあります。
grpc/grpc-go - GitHub
gRPC-Go The Go implementation of gRPC
https://github.com/grpc/grpc-go
今日はこれを使って、gRPC 通信を試してみたいと思います。
まず proto ファイルを作ります。Protocol Buffers の proto ファイルの作り方については過去に何度かやったのと同じです。
proto ファイルは以下の通り。
customer_service.proto
syntax = "proto3";
package proto;
service CustomerService {
rpc ListPerson(RequestType) returns (stream Person) {};
rpc AddPerson(Person) returns (ResponseType) {}
}
message ResponseType {
}
message RequestType {
}
message Person {
string name = 1;
int32 age = 2;
}
Protocol Buffers のバージョンが上がり、service の定義も書ける様になりました。定義内容は Person というデータ構造に対して、一覧に追加する AddPerson
、一覧を返す ListPerson
を定義しました。
この proto ファイルからスタブを作ります。スタブは protoc というコマンドを使うのですが、grpc-go の場合は protoc にデフォルトで組み込まれていない為、protoc のプラグインとして動作させる必要があります。
まず protoc をダウンロードしてきます。Google のサイトに置いてある物は stable release なので今回の service は使えません。ご注意ください。github 上の alpha リリース物をダウンロードします。
Releases - google/protobuf - GitHub
https://github.com/google/protobuf/releases
ダウンロードした protoc をパスの通った所に置いて下さい。
$ go get github.com/golang/protobuf/protoc-gen-go
これで grpc-go が protoc のプラグインとしてインストール出来ます。
protoc --go_out=plugins=grpc:. customer_service.proto
すると、以下の customer_service.pb.go
というファイルが生成されます。
package proto
import proto1 "github.com/golang/protobuf/proto"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
var _ context.Context
var _ grpc.ClientConn
var _ = proto1.Marshal
type ResponseType struct {
}
func (m *ResponseType) Reset() { *m = ResponseType{} }
func (m *ResponseType) String() string { return proto1.CompactTextString(m) }
func (*ResponseType) ProtoMessage() {}
type RequestType struct {
}
func (m *RequestType) Reset() { *m = RequestType{} }
func (m *RequestType) String() string { return proto1.CompactTextString(m) }
func (*RequestType) ProtoMessage() {}
type Person struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age" json:"age,omitempty"`
}
func (m *Person) Reset() { *m = Person{} }
func (m *Person) String() string { return proto1.CompactTextString(m) }
func (*Person) ProtoMessage() {}
func init() {
}
type CustomerServiceClient interface {
ListPerson(ctx context.Context, in *RequestType, opts ...grpc.CallOption) (CustomerService_ListPersonClient, error)
AddPerson(ctx context.Context, in *Person, opts ...grpc.CallOption) (*ResponseType, error)
}
type customerServiceClient struct {
cc *grpc.ClientConn
}
func NewCustomerServiceClient(cc *grpc.ClientConn) CustomerServiceClient {
return &customerServiceClient{cc}
}
func (c *customerServiceClient) ListPerson(ctx context.Context, in *RequestType, opts ...grpc.CallOption) (CustomerService_ListPersonClient, error) {
stream, err := grpc.NewClientStream(ctx, &_CustomerService_serviceDesc.Streams[0], c.cc, "/proto.CustomerService/ListPerson", opts...)
if err != nil {
return nil, err
}
x := &customerServiceListPersonClient{stream}
if err := x.ClientStream.SendProto(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type CustomerService_ListPersonClient interface {
Recv() (*Person, error)
grpc.ClientStream
}
type customerServiceListPersonClient struct {
grpc.ClientStream
}
func (x *customerServiceListPersonClient) Recv() (*Person, error) {
m := new(Person)
if err := x.ClientStream.RecvProto(m); err != nil {
return nil, err
}
return m, nil
}
func (c *customerServiceClient) AddPerson(ctx context.Context, in *Person, opts ...grpc.CallOption) (*ResponseType, error) {
out := new(ResponseType)
err := grpc.Invoke(ctx, "/proto.CustomerService/AddPerson", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
type CustomerServiceServer interface {
ListPerson(*RequestType, CustomerService_ListPersonServer) error
AddPerson(context.Context, *Person) (*ResponseType, error)
}
func RegisterCustomerServiceServer(s *grpc.Server, srv CustomerServiceServer) {
s.RegisterService(&_CustomerService_serviceDesc, srv)
}
func _CustomerService_ListPerson_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(RequestType)
if err := stream.RecvProto(m); err != nil {
return err
}
return srv.(CustomerServiceServer).ListPerson(m, &customerServiceListPersonServer{stream})
}
type CustomerService_ListPersonServer interface {
Send(*Person) error
grpc.ServerStream
}
type customerServiceListPersonServer struct {
grpc.ServerStream
}
func (x *customerServiceListPersonServer) Send(m *Person) error {
return x.ServerStream.SendProto(m)
}
func _CustomerService_AddPerson_Handler(srv interface{}, ctx context.Context, buf []byte) (proto1.Message, error) {
in := new(Person)
if err := proto1.Unmarshal(buf, in); err != nil {
return nil, err
}
out, err := srv.(CustomerServiceServer).AddPerson(ctx, in)
if err != nil {
return nil, err
}
return out, nil
}
var _CustomerService_serviceDesc = grpc.ServiceDesc{
ServiceName: "proto.CustomerService",
HandlerType: (*CustomerServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "AddPerson",
Handler: _CustomerService_AddPerson_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "ListPerson",
Handler: _CustomerService_ListPerson_Handler,
ServerStreams: true,
},
},
}
あとは golang の開発手順通り、これを参照したサーバとクライアントを作ります。まずサーバ
package main
import (
"golang.org/x/net/context"
"log"
"net"
"sync"
"google.golang.org/grpc"
pb "github.com/mattn/grpc-example/proto"
)
type customerService struct {
customers []*pb.Person
m sync.Mutex
}
func (cs *customerService) ListPerson(p *pb.RequestType, stream pb.CustomerService_ListPersonServer) error {
cs.m.Lock()
defer cs.m.Unlock()
for _, p := range cs.customers {
if err := stream.Send(p); err != nil {
return err
}
}
return nil
}
func (cs *customerService) AddPerson(c context.Context, p *pb.Person) (*pb.ResponseType, error) {
cs.m.Lock()
defer cs.m.Unlock()
cs.customers = append(cs.customers, p)
return new(pb.ResponseType), nil
}
func main() {
lis, err := net.Listen("tcp", ":11111")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
server := grpc.NewServer()
pb.RegisterCustomerServiceServer(server, new(customerService))
server.Serve(lis)
}
何の面白味もないフツーに RPC のコードですね。一覧を返す場合は、配列ではなく stream に対して書き込む事で一覧を返す事になります。またどうやら Protocol Buffers では引数の無いインタフェースは作れない様なので、RequestType と ResponseType という型も用意していますが、要らないからといって nil を返したりすると落ちるので要注意です。
次にクライアント
package main
import (
"fmt"
"io"
"strconv"
pb "github.com/mattn/grpc-example/proto"
"github.com/mattn/sc"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
func add(name string, age int) error {
conn, err := grpc.Dial("127.0.0.1:11111")
if err != nil {
return err
}
defer conn.Close()
client := pb.NewCustomerServiceClient(conn)
person := &pb.Person{
Name: name,
Age: int32(age),
}
_, err = client.AddPerson(context.Background(), person)
return err
}
func list() error {
conn, err := grpc.Dial("127.0.0.1:11111")
if err != nil {
return err
}
defer conn.Close()
client := pb.NewCustomerServiceClient(conn)
stream, err := client.ListPerson(context.Background(), new(pb.RequestType))
if err != nil {
return err
}
for {
person, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
fmt.Println(person)
}
return nil
}
func main() {
(&sc.Cmds{
{
Name: "list",
Desc: "list: listing person",
Run: func(c *sc.C, args []string) error {
return list()
},
},
{
Name: "add",
Desc: "add [name] [age]: add person",
Run: func(c *sc.C, args []string) error {
if len(args) != 2 {
return sc.UsageError
}
name := args[0]
age, err := strconv.Atoi(args[1])
if err != nil {
return err
}
return add(name, age)
},
},
}).Run(&sc.C{})
}
最近開発中の、サブコマンドライブラリ sc を使ってみました。サーバが stream から Send で送ってくるのでクライアントもループで回しながら Recv する必要があります。
ちゃんと動きました。固い型付けの言語でしっかり動くと気持ちいいですね。今後いろいろなアプリケーションが grpc を使い、HTTP/2 が透過的に使われていく時代になっていくのだと思います。ワクワクしますね。
今日作った物は以下のリポジトリに置いてあります。よろしければ遊んで下さい。
mattn/grpc-example - GitHub
https://github.com/mattn/grpc-example