Edited at

Goで始めるgRPC入門


なにこれ?

昔どこかに書いた記事が吹っ飛んで悲しかったので、こちらに復帰。

Goを使ってgRPCのServer,Clientを実装する記事となります。


gRPC?

grpc.png

gRPCは、Googleによって開発されたRPCフレームワークです。

HTTP/2を使用した通信部分のライブラリ(ProtocolBuffersでシリアライズ)とProtocolBuffers(標準)としたテンプレートコードの生成がセットで提供されています。

ざっくりと言っちゃうと、HTTP/2を使った手続き部分がばっくり提供されていて

Server,ClientのコードはProtocみたいなエコシステムでgenerate

できる、という省エネでHTTP/2に乗れる仕組みです。わーい。

grpc-arch.png

HTTP/2のstreamもサポートしています。

gRPCのサポートするRPC方式は以下の通り。


  • Unary RPC (1リクエスト1レスポンス)

  • Server streaming RPC (1つのリクエストに複数レスポンス)

  • Client streaming RPC (複数のリクエストに一つのレスポンス)

  • Bidirectional streaming RPC(双方向)

対応言語は以下の通りです。


  • C++

  • go

  • Ruby

  • Android Java

  • PHP

  • Objective-C


下準備


gRPCのインストール

go get -u google.golang.org/grpc


protocのインストール

protoファイルからコード生成をするコンパイラ(protoc)をインストールします。

protocのダウンロードはos別にこちらから

PATHの通ったディレクトリに解凍したディレクトリの/bin の中のバイナリを

移してあげてください。


protocのGo用のプラグインをインストール

go get -u github.com/golang/protobuf/protoc-gen-go


Lets実装


.protoファイルにインターフェースを定義する

gRPCをベースにした開発では、まずはIDLを使ってprotoにAPIの定義を書きます。

ProtocolBuffer以外もサポートはしているようですが、

ツール周りやドキュメントが一番手厚いし、標準に寄り添って行きたい民なので、今回はprotocolBufferで.protoファイルを作ります。


syntax = "proto3";
service Cat {
rpc GetMyCat (GetMyCatMessage) returns (MyCatResponse) {}
}
message GetMyCatMessage {
string target_cat = 1;
}
message MyCatResponse {
string name = 1;
string kind = 2;
}

proto3の型や各言語の型の対応はgoogleのドキュメント、基本のscalar型以外を使いたい場合はgoogle/protobufをimportする感じで。


.proto ファイルからserver、client,interface等のコードを生成する

定義から各言語のベースとなるコードの自動生成をします。

protocコマンドを実行します。

protoc --go_out=plugins=grpc:../pb cat.proto

成功すると xxxx.pb.go が生成されます。

xxxx.pb.go ファイルには、protoで定義した以下が含まれています。


  • request

  • response

  • client,serverのinterface

  • registerMethod

プラットフォームまたいでも、同一の定義からこの辺のコードが生成できるのは楽ですね :)

proto3のscalar型がgoのtime型やint型などに対応してたら嬉しかったのですが、今時点では対応してなかったのがやや辛み...


protocによるdocument生成

protocでdocumentの生成もできます。わーい。


プラグインをインストール

go get -u github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc


document生成

protoc --doc_out=html,index.html:./ proto/*.proto


pb.goファイルを参照してサーバーとクライアントの実装

生成されたpb.goに含まれるinterfaceに沿って、

実処理とserverとclientを実装します :)


service (実処理)

作られたxxx.pb.goのinterfaceを満たすように実装します。

xxx.pb.go

type CatServer interface {
GetMyCat(context.Context, *GetMyCatMessage) (*MyCatResponse, error)
}

serviceって名称は、公式やprotoの呼称から取ってきただけなのでお好みで。


package service
import (
"context"
"errors"
pb "marnie_playground/grpc-sample/pb"
)
type MyCatService struct {
}
func (s *MyCatService) GetMyCat(ctx context.Context, message *pb.GetMyCatMessage) (*pb.MyCatResponse, error) {
switch message.TargetCat {
case "tama":
//たまはメインクーン
return &pb.MyCatResponse{
Name: "tama",
Kind: "mainecoon",
}, nil
case "mike":
//ミケはノルウェージャンフォレストキャット
return &pb.MyCatResponse{
Name: "mike",
Kind: "Norwegian Forest Cat",
}, nil
}
return nil, errors.New("Not Found YourCat")
}


server & client

gRPC関連で書く必要があるServerのコードは最小だと以下だけです。


  • port listen

  • 作った実処理の登録,serve

middleware的なことをしたい場合はintercepterをつかいませう。


  • server


package main
import (
"log"
"net"
pb "marnie_playground/grpc-sample/pb"
"marnie_playground/grpc-sample/service"
"google.golang.org/grpc"
)
func main() {
listenPort, err := net.Listen("tcp", ":19003")
if err != nil {
log.Fatalln(err)
}
server := grpc.NewServer()
catService := &service.MyCatService{}
// 実行したい実処理をseverに登録する
pb.RegisterCatServer(server, catService)
server.Serve(listenPort)
}


  • client


client
package main
import (
"context"
"fmt"
"log"
pb "marnie_playground/grpc-sample/pb"
"google.golang.org/grpc"
)
func main() {
//sampleなのでwithInsecure
conn, err := grpc.Dial("127.0.0.1:19003", grpc.WithInsecure())
if err != nil {
log.Fatal("client connection error:", err)
}
defer conn.Close()
client := pb.NewCatClient(conn)
message := &pb.GetMyCatMessage{TargetCat: "tama"}
res, err := client.GetMyCat(context.TODO(), message)
fmt.Printf("result:%#v \n", res)
fmt.Printf("error::%#v \n", err)
}

FactoryMethodとClientInterfaceが提供されているので、Connection作成してメソッドを呼び出すだけで、KANTAN :)


ビルド & テスト

出来あがったclientとserverをそれぞれgo build,実行すれば出来上がり。

./client

result:&cat.MyCatResponse{Name:"tama", Kind:"mainecoon"}
error::<nil>


  • middleware(interceptor)

とはいえ、認証とかロギングみたいな横断的な関心ごとを実装することを

考えるとmiddlewareとかほしいですよね。

logging,auth,recovery的な物はinterceptorとかmiddleware的な物を作ってやれると、既存のプロジェクトからの移行もスムーズかなーと思っていたので、調べてみました。

interceptorは型定義されてますので、下記を満たすようなインターフェースで実装します。

google.golang.org/grpc/interceptor.go

//UnaryRPCならこっち
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
//StreamRPCならこっち
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

利用するRPC方式によってgrpc.UnaryServerInterceptor()ないし grpc.StreamInterceptor()grpc.ServerOptionに変換すればOKです :)

大雑把なイメージは以下のような感じ。


func MiddlewareFunc(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error)
// 処理かく
}
func main() {
//中略
middleware := &YourMiddleware{}
opt := []grpc.ServerOption{grpc.UnaryInterceptor(MiddlewareFunc)}
server := grpc.NewServer(opt...)
}

grpc-middleware に何個か実装されたmiddleware(cf. logrus,validator)がありますので、

独自のmiddlewareを実装する場合はこの辺参考にすれば良いのかなと思います :)


まとめ(いいとこ、わるいとこ)


  • 主要な通信層の処理は提供されているので、HTTP/2関連の実装は必要ない。

  • protoからボイラーテンプレートコードやドキュメントも作られるのは楽。

  • middlewareも増えている。

  • コードの自動生成によって本来時間をかけるべき、機能実装に集中できる

  • HTTP/2による高速化/stream用途や、protobuff標準でのコード生成という所が包括的に提供されるといった恩恵を受けられる

フルスタックフレームワークと較べるのは、主旨が異なるので要件や選定において重視する所次第って感じですが、マイクロフレームワークを使うような構成を検討していたり、ストリーミング通信が要件に含まれているのであれば採用するメリットはあるかなと。

細々、下記のようなところは気になりました。


  • proto3がgoの型を全て網羅してるわけではなさそうなので、applicationLayerの既存コードの型合わない場合は変換層とか必要そう

  • curlでポチッと、みたいなのが使えなくなったので、テストがちょっと大変。grpc-gatewayでも使うべきなのかしら。

まぁデメリットと言うほどではないかな〜って気も。

テスト手段やエコシステム、ミドルウェアは今後、充実していく気もするし、既に必要十分ではあると思うので。


追記

grpc_cliもあるので最近はテストの敷居も下がってきている感じですね。