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 して下さい。


2015/01/23


lingr で動かしてたしりとりbotをtwitterでも動く様にしてみました。

mattn/siritori-bot - GitHub
https://github.com/mattn/siritori-bot

golang で書かれています。sirtori_bot につぶやくと...

といった具合にしりとりで返信してくれます。辞書データは以下のURLにある「萌色用語の基礎知識」を利用させて頂きました。

通信用語の基礎知識 ダウンロード

ファイル 本体 マニュアルと辞書本体です。 マニュアル 通信用語の基礎知識(WDIC) 電算用語の基礎知識(TECH) 科学用語の基礎知識(SCI) 萌色用語の基礎知識(MOE) 文化用語の基礎知識(...

http://www.wdic.org/download.shtml

ソースコードで参考になる所はほぼありませんが、適当に遊んで下さい。