Go言語でgRPCを簡単なgRPCサーバを立てる

gRPCとは
Googleが2015年に発表したHTTP/2を使ったRPC(Remote Procedure Call, 遠隔手続き呼出し)フレームワークです。
Protocol Bufferでデータをシリアライズをすることによってデータサイズを小さくすることによって通信料を減らしたり、インターフェースの定義ファイル(.proto)から様々な言語にコードを生成することができるため、静的型付言語などでJSONに対応する型を書く手間が省けたり、型をクライアント側に強制することができます。
gRPCは、バックエンド間の通信などに使われ、最近は大きなシステムの開発にマイクロサービスアーキテクチャ(大きなシステムのスケーラビリティの向上のために複数の小さいサービスに分割して連携することで機能を実装を行うアーキテクチャ)が採用されることが増えており、それに伴ってよりgRPCが重宝されています。
環境
$ go version
go version go1.10.4 darwin/amd64
gRPCのコード生成にprotocコマンドを用いるため、インストール
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ protoc --version
libprotoc 3.6.0
.protoファイルの作成
Protocol BuffersのIDL(インターフェース定義言語)でgRPCのインターフェースの定義を行います。
このファイルによって、様々な言語のソースコードの雛形を生成することができ、通信するデータのパースが簡単に行えます。
以下のようなファイルを定義します。
sample.proto
syntax = "proto3";
service Sample {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
service
:サービス定義
今回はSayHelloという、受け取った引数にHelloという文字列を追加した文字列を返すメソッドを作るので、そのインターフェースの定義をします。
message
: 引数や出力の型の宣言
作成した定義ファイルからGo言語のコード生成を行います。
生成するコードを格納するディレクトリの生成
$ mkdir pb
$ mkdir pb/sample
$ protoc --go_out=plugins=grpc:pb/sample sample.proto
これによって、sample.pg.go
というファイルが生成され、構造体やインターフェースが定義されたファイルが生成されます。
構造体
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
インターフェース
// SampleServer is the server API for Sample service.
type SampleServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
実装
先ほど作成したインターフェースの実際の処理を行うサーバー側とそのgRPCのメソッドを呼び出すクライアントのコードの実装します。
公式が用意しているサンプルコードを参考にして作成します。
ファイル構成
.
├── client
│ └── main.go
├── pb
│ └── sample
│ └── sample.pb.go
├── sample.proto
└── server
└── main.go
サーバー側のコード
サーバー側はインターフェースで定義したメソッドの処理を実際に行うコードを作成します。
package main
import (
"context"
"log"
"net"
pb "../pb/sample"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
port = ":50051"
)
type server struct{}
// 作成したインターフェースの実装
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello, " + in.Name + "!!"}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterSampleServer(s, &server{})
// Register reflection service on gRPC server.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
先ほど.protoファイルから生成したインターフェースのSayHello
を実装して、net.Listen("tcp", port)
の箇所でtcpポートを開いてサーバーを立ち上げています。
クライアント側のコード
クライアント側は、先ほど立ち上げたサーバーに接続して、SayHello
を呼び出します。
package main
import (
"context"
"log"
"os"
"time"
pb "../pb/sample"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultName = "Taro"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewSampleClient(conn)
// Contact the server and print out its response.
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
grpc.Dial
の箇所で先ほど立ち上げたgRPCサーバーに接続を行い、.protoファイルから生成されたコードのメソッドに、宣言した型を渡すことによって先ほど処理を追加したSayHello
を実行します。
実行結果
サーバー側をgo run main.go
で実行した状態で、クライアント側のコードを実行します。
$ go run main.go
2018/11/22 19:48:45 Greeting: Hello, Taro!!
まとめ
サンプルコードのなぞり書きで、深いところまでは全然理解できていませんが、型を異なるプログラミング言語のクライアント側とサーバー側で共有できるというのはなかなか便利そうでした。
サーバー間の連携を行うためのAPIを作る際、REST APIでは設計や型、JSONをパースするためにコードを書くのがキツく感じてきた際などは、かなり使えそうな感じがしました。