2015/02/27

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

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 というファイルが生成されます。

// Code generated by protoc-gen-go.
// source: customer_service.proto
// DO NOT EDIT!

/*
Package proto is a generated protocol buffer package.

It is generated from these files:
    customer_service.proto

It has these top-level messages:
    ResponseType
    RequestType
    Person
*/
package proto

import proto1 "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 _ = 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() {
}

// Client API for CustomerService service

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
}

// Server API for CustomerService service

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 interror {
    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 []stringerror {
                return list()
            },
        },
        {
            Name: "add",
            Desc: "add [name] [age]: add person",
            Run: func(c *sc.C, args []stringerror {
                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
Posted at by