2015/03/04


先日、Google が開発しているリモートプロシージャコール、gRPC を golang から使うチュートリアルを書きましたが

Big Sky :: Protocol Buffers を利用した RPC、gRPC を golang から試してみた。
http://mattn.kaoriya.net/software/lang/go/20150227144125.htm

今日は ruby と C++ から触ってみたいと思います。はじめに ruby の方ですが、Ruby 2.2.0 でビルドする事が出来ません。どうしても Ruby 2.2.0 から試したい人は、以下の PR にあるパッチを適用して下さい。

Support ruby 2.2.0 by mattn · Pull Request #894 - grpc/grpc - GitHub
https://github.com/grpc/grpc/pull/894

今回の検証は ruby 2.1.0 で行いました。まず proto ファイルから ruby のスタブを吐くには以下の様に実行します。

$ protoc --ruby_out=. --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_ROOT)/bins/opt/grpc_ruby_plugin customer_service.proto

GRPC_ROOT は grpc を checkout したディレクトリですが、システムにインストールした人は適便書き換えて下さい。golang の時の様にスタブが吐かれるのでサーバであれば

#!/usr/bin/env ruby

require 'grpc'
require 'customer_service_services'

class MyServer < Proto::CustomerService::Service
  def initialize()
    @customers = []
  end
  def add_person(arg, call)
    @customers << arg
    Proto::ResponseType.new
  end
  def list_person(args, call)
    @customers
  end
end

def main
  customers = []
  server = GRPC::RpcServer.new
  server.add_http2_port('0.0.0.0:11111')
  server.handle(MyServer.new)
  server.run
end

main

こんな感じ。クライアントであれば

#!/usr/bin/env ruby

require 'grpc'
require 'customer_service_services'

def main
  stub = Proto::CustomerService::Stub.new('localhost:11111')
  if ARGV.size == 2
    stub.add_person(Proto::Person.new(nameARGV[0], ageARGV[1].to_i))
  else
    stub.list_person(Proto::RequestType.new).each do |x|
      puts "name=#{x.name}, age=#{x.age}"
    end
  end
end

main

こんな感じに実装して下さい。今回は Sync サーバで実装しましたが、Async サーバで実装する場合は customers に排他を行うべきです。

C++ もやり方は変わりません。

protoc --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_ROOT)/bins/opt/grpc_cpp_plugin customer_service.proto

protoc でスタブを吐いてサーバであれば

#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <grpc/grpc.h>
#include <grpc++/server.h>
#include <grpc++/server_builder.h>
#include <grpc++/server_context.h>
#include <grpc++/status.h>
#include <grpc++/stream.h>
#include "customer_service.pb.h"

using namespace proto;
using namespace grpc;

class CustomerServiceImpl final : public CustomerService::Service {
private:
  std::vector<Person> customers;
public:
  Status AddPerson(ServerContext* context, const Person* customer, ResponseType* response) {
    std::cout << "AddPerson" << std::endl;
    customers.push_back(*customer);
    std::cout << "Done" << std::endl;
    return Status::OK;
  }

  Status ListPerson(ServerContext* context, const RequestType* request, ServerWriter<Person>* writer) {
    std::cout << "ListPerson" << std::endl;
    for (const Person& p : customers) {
      writer->Write(p);
    }
    std::cout << "Done" << std::endl;
    return Status::OK;
  }
};

int
main(int argc, char* argv[]) {
  grpc_init();
  std::string server_address("0.0.0.0:11111");
  CustomerServiceImpl service;

  ServerBuilder builder;
  builder.AddPort(server_address);
  builder.RegisterService(&service);
  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;
  server->Wait();
  grpc_shutdown();
  return 0;
}
またクライアントであれば
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <grpc/grpc.h>
#include <grpc++/channel_arguments.h>
#include <grpc++/channel_interface.h>
#include <grpc++/client_context.h>
#include <grpc++/create_channel.h>
#include <grpc++/status.h>
#include <grpc++/stream.h>
#include "customer_service.pb.h"

using namespace proto;
using namespace grpc;

int
main(int argc, char** argv) {
  grpc_init();
  std::unique_ptr<CustomerService::Stub> client = CustomerService::NewStub(
      grpc::CreateChannelDeprecated("127.0.0.1:11111", ChannelArguments()));
  ClientContext context;
  RequestType request;
  ResponseType response;
  Person person;

  Status status;
  if (argc == 3) {
    person.set_name(argv[1]);
    person.set_age(atoi(argv[2]));
    status = client->AddPerson(&context, person, &response);
  } else {
    std::unique_ptr<ClientReader<Person>> reader = client->ListPerson(&context, request);
    while (reader->Read(&person)) {
      std::cout << "name=" << person.name() << ", age=" << person.age() << std::endl;
    }
    status = reader->Finish();
  }
  if (!status.IsOk()) {
    std::cout << "ListFeatures rpc failed." << std::endl;
  }
  client.reset();
  grpc_shutdown();
}

この様に実装します。同じ proto ファイルから生成したサーバやクライアントですので、ruby のサーバを起動して、golang のクライアントや C++ のクライアントからリクエストを送る事も出来ますし、C++ をサーバや golang をサーバにしても良いでしょう。

ただ触った感じですが、ruby のサーバは何故かレスポンスが悪かったので調査を兼ねてベンチマークを取ってみました。

シナリオは、ruby、C++、golang それぞれのサーバを起動して C++ のクライアントから要求します。計測は AddPerson の1000回呼び出し、とその1000件入った状態で ListPerson を100回呼び出しを計測しました。

ruby

mattn/grpc-example-rb - GitHub

AddPerson

$ time seq 1000 | xargs -n 1 ./client mattn

real    0m14.287s
user    0m6.667s
sys     0m4.607s

ListPerson

$ time seq 100 | xargs -n 1 ./client > /dev/null

real    0m12.385s
user    0m4.833s
sys     0m4.367s

cpp

mattn/grpc-example-cpp - GitHub

AddPerson

$ time seq 1000 | xargs -n 1 ./client mattn

real    0m12.326s
user    0m6.007s
sys     0m4.710s

ListPerson

$ time seq 100 | xargs -n 1 ./client > /dev/null

real    0m4.304s
user    0m2.430s
sys     0m1.490s

golang

mattn/grpc-example - GitHub

AddPerson

$ time seq 1000 | xargs -n 1 ./client mattn

real    0m13.067s
user    0m6.553s
sys     0m4.737s

ListPerson

$ time seq 100 | xargs -n 1 ./client > /dev/null

real    0m5.755s
user    0m2.767s
sys     0m1.983s

とまぁ、想定通りの結果が出ました。golang が案外頑張ってるなーという印象です。今後時間が出来たら Java や python も触っていく予定です。これまで3言語を同じ proto ファイルからスタブ生成して開発を行ってみましたが、各々扱い方が異なるのでちょっと混乱する可能性があります。自分の好きな言語で実装するのが良いと思います。

サーバ/インフラ徹底攻略 (WEB+DB PRESS plus) サーバ/インフラ徹底攻略 (WEB+DB PRESS plus)
伊藤 直也
技術評論社 / ¥ 2,138 (2014-10-30)
 
発送可能時間:在庫あり。

WEB+DB PRESS Vol.85 WEB+DB PRESS Vol.85
菅原 元気
技術評論社 / ¥ 1,598 (2015-02-24)
 
発送可能時間:在庫あり。


2015/02/27


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

2015/02/23


SPA (Single Page Application) を書いていると、いちいちブラウザをリロードするのが面倒で、かつ js を minify してページをリロードするといった面倒な手間を出来れば何も設定せずにやりたい(もしくは微量な設定だけでやりたい)、という思いから goemon というツールを書きました。

mattn/goemon - GitHub
https://github.com/mattn/goemon

goemon は、コマンドラインツールとして使います。まず

$ goemon -g > goemon.yml

goemon.yml を生成します。個人的にカスタマイズしたい人は生成されたファイルを変更して使って下さい。

# Generated by goemon -g
livereload: :35730
tasks:
match: './assets/*.js'
  commands:
  minifyjs -m -i ${GOEMON_TARGET_FILE} > ${GOEMON_TARGET_DIR}/${GOEMON_TARGET_NAME}.min.js
  :livereload /
match: './assets/*.css'
  commands:
  :livereload /
match: './assets/*.html'
  commands:
  :livereload /
match: '*.go'
  commands:
  go build
  :restart
  :livereload /
$ goemon ./foo

foo が起動します。サーバを起動するコマンドを渡す事になります。デフォルトの設定だと以下の様に動作します。

  • assets/*.js が変更されたら同じディレクトリに minify した js(拡張子: .min.js) を生成し、/ を livereload (自動再読み込み)する。
  • assets/*.htmlassets/*.css が変更されたら / を livereload する。
  • *.go が変更されたら go build してアプリを再起動(引数を再実行)した後、/ を livereload する。

つまりは開発者はエディタで編集するだけでブラウザ勝手にリロードしてウマーーー!という想定

別に golang だけのツールという訳では無いので、ruby や node/io.js 等でも使えます。ワイルカードパターンには **/*.js という複数ディレクトリ階層のワイルドカードも使えます。もちろんですが、Windows でも動作します。詳しくは README を参照下さい。

goemon で使える便利なパターンやコマンドがあれば、ぜひ pull-request して下さい。