概要
gRPCはあらゆる環境で実行できるモダンで高性能なオープンソースRPC(Remoto Protocol Call)フレームワークです。Protocol Buffersを使ってデータをシリアライズし、高速な通信を実現できるという利点がある。様々な言語やプラットフォームに対応しており、http/2を活かした双方向ストリーミングなどが可能である。Protocol Buffersを使用してシンプルにサービス(通信するデータや関数)を定義でき、APIの仕様を明文化できる。
※この章はgRPCを参考にしています。
git: k-washi/example-golang-gRPC
スタートプロジェクト
VSCodeの設定
- vscode-proto3 & Clang-Format extensions をインストール
もし、自動でファイルをフォーマットしたいならば、以下も設定する。
- Clang-Formatをインストール
brew install clang-format #MacOSX
#other OS
#http://www.codepool.biz/vscode-format-c-code-windows-linux.html
Protocol buffersの設定
Protocol buffersのインストール
# MacOSX
brew install protobuf #MacOSX
# Linux
#https://github.com/google/protobuf/releases
#example
# Make sure you grab the latest version
curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip
# Unzip
unzip protoc-3.5.1-linux-x86_64.zip -d protoc3
# Move protoc to /usr/local/bin/
sudo mv protoc3/bin/* /usr/local/bin/
# Move protoc3/include to /usr/local/include/
sudo mv protoc3/include/* /usr/local/include/
# Optional: change owner
sudo chown [user] /usr/local/bin/protoc
sudo chown -R [user] /usr/local/include/google
Golangの設定
パッケージののインストール
go get -u google.golang.org/grpc
go get -d -u github.com/golang/protobuf/protoc-gen-go
実行の確認
まず、サービスを定義する。
syntax = "proto3";
package greet;
option go_package="greetpb";
service GreetService{}
この定義したファイルに対して、以下のコマンドでgolang用のprotocol buffers(greet.pb.go)に変換する。この変換されたライブラリファイル(greet.pb.go)内の関数を、golangによるクライアントとサーバーで使用することで、gRPCによる通信を実現できる。
protoc greet/greetpb/greet.proto --go_out=plugins=grpc:.
クライアントは、以下のように接続の確立を行い、クライアントAPI(NewGreetServiceClient)で通信している。
package main
import (
"fmt"
"log"
//protocol buffersライブラリー
"github.com/k-washi/example-golang-gRPC/greet/greetpb"
"google.golang.org/grpc"
)
func main() {
fmt.Println("Hello. I'm a client")
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Could not connect: %v", err)
}
defer conn.Close()
c := greetpb.NewGreetServiceClient(conn)
fmt.Printf("Created client %f", c)
}
サーバーは、構造体serverを定義し、サービスをサーバーに登録する。そして、サーバーを立ち上げる。
package main
import (
"fmt"
"log"
"net"
"github.com/k-washi/example-golang-gRPC/greet/greetpb"
"google.golang.org/grpc"
)
type server struct{}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
greetpb.RegisterGreetServiceServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
以下のコマンドで、サーバーを立ち上げ、クライアントを実行することで、実行の確認を行う。
#server setup
go run greet/greet_server/server.go
#client setup
go run greet/greet_client/client.go
通信手法
gRPCは、以下のような通信手法があり、データの特性に応じて適宜選択する必要がある。
- Unary - もっとも一般的なgRPCのAPIで、クライアントからのメッセージ1つに対して、サーバーは1回のメッセージを返す
- Server streaming - HTTP/2の恩恵を受けたAPIで、クライアントからのメッセージ1つに対して、サーバーは複数回のメッセージを返す
- Client streaming - HTTP/2の恩恵を受けたAPIで、クライアントからの複数のメッセージに対して、サーバーは1回のメッセージを返す
サービスの定義
- 送信するメッセージの型を定義する
- Unary, Server streaming, Client streamingのリクエスト・レスポンスにおけるメッセージの型を定義する
- サービスを rpc サービス名(stream クライアントの型) returns (stream レスポンスの型) {}; として定義する。 複数回送るサービスのメッセージにはstreamをつける。
- 以下のコマンドでprotocol buffersに変換.
protoc streaming/greetpb/greet.proto --go_out=plugins=grpc:.
syntax = "proto3";
package greet;
option go_package="greetpb";
//リクエストメッセージの型を定義
message Greeting {
string first_name = 1;
string last_name = 2;
}
// Unary リクエスト
message GreetRequest {
Greeting greeting = 1;
}
// Unary レスポンス
message GreetResponse {
string result = 1;
}
// Server streaming リクエスト
message GreetManyTimesRequest {
Greeting greeting = 1;
}
// Server streaming レスポンス
message GreetManyTimesResponse {
string result = 1;
}
// Client streaming リクエスト
message LongGreetRequest {
Greeting greeting = 1;
}
// Client streaming レスポンス
message LongGreetResponse {
string result = 1;
}
// サービスの定義
service GreetService{
//Unary
rpc Greet(GreetRequest) returns (GreetResponse) {};
//Server Streaming
rpc GreetManyTimes(GreetManyTimesRequest) returns (stream GreetManyTimesResponse) {};
//Client Streaming
rpc LongGreet(stream LongGreetRequest) returns (LongGreetResponse) {};
}
クライアント
Golangにより、Unary, Server streaming, Client streamingのクライアントを実装する。基本的には、サービス定義に合わせた型でデータを作成、リクエストを送信、そして、レスポンスを待つ形式である。これらの処理には、protocol buffersライブラリに記載されたパッケージを使用する。サーバーからの複数回のメッセージを待つ場合、繰り返し構文(for)を用いて受信することで、適用できる。複数回サーバーにメッセージを送信する場合も、繰り返し構文を用いているが、基本的には、メッセージごとに送信しているだけである。gRPCに関するエラーは、"google.golang.org/grpc/status"ライブラリを用いて抽出・定義できる。
package main
import (
"context"
"fmt"
"io"
"log"
"time"
"google.golang.org/grpc/codes"
"github.com/k-washi/example-golang-gRPC/streaming/greetpb"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
func main() {
fmt.Println("Hello. I'm a client")
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Could not connect: %v", err)
}
defer conn.Close()
c := greetpb.NewGreetServiceClient(conn)
doUnary(c)
doServerStreaming(c)
doClientStreaming(c)
}
func doUnary(c greetpb.GreetServiceClient) {
fmt.Println("Starting to do a Unary RPC ...")
req := &greetpb.GreetRequest{
Greeting: &greetpb.Greeting{
FirstName: "tanaka",
LastName: "tarou",
},
}
res, err := c.Greet(context.Background(), req)
if err != nil {
respErr, ok := status.FromError(err)
if ok {
fmt.Println(respErr.Message())
fmt.Println(respErr.Code())
if respErr.Code() == codes.InvalidArgument {
fmt.Println("We probably sent a empty string")
} else if respErr.Code() == codes.DeadlineExceeded {
fmt.Println("Timeout was hit! Deadline was exceeded")
}
} else {
log.Printf("error while calling greet rpc: %v", err)
}
}
fmt.Printf("Response from greet server %v", res)
}
func doServerStreaming(c greetpb.GreetServiceClient) {
fmt.Println("Starting to do a Server Streaming RPC ...")
req := &greetpb.GreetManyTimesRequest{
Greeting: &greetpb.Greeting{
FirstName: "tanaka",
LastName: "tarou",
},
}
resStream, err := c.GreetManyTimes(context.Background(), req)
if err != nil {
log.Fatalf("error while calling GreetManyTImes RPC: %v", err)
}
for {
msg, err := resStream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("error while reading stream: %v", err)
}
log.Printf("Response from GreetManyTImes: %v", msg.GetResult())
}
}
func doClientStreaming(c greetpb.GreetServiceClient) {
fmt.Println("Starting to do a Client Streaming RPC")
requests := []*greetpb.LongGreetRequest{
&greetpb.LongGreetRequest{
Greeting: &greetpb.Greeting{
FirstName: "one",
},
},
&greetpb.LongGreetRequest{
Greeting: &greetpb.Greeting{
FirstName: "two",
},
},
&greetpb.LongGreetRequest{
Greeting: &greetpb.Greeting{
FirstName: "three",
},
},
&greetpb.LongGreetRequest{
Greeting: &greetpb.Greeting{
FirstName: "four",
},
},
}
stream, err := c.LongGreet(context.Background())
if err != nil {
log.Fatalf("error while calling LongGreet %v", err)
}
for _, req := range requests {
fmt.Printf("Sending req: %v\n", req)
stream.Send(req)
time.Sleep(1000 * time.Millisecond)
}
res, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("error while receiving response from LongGreet: %v", err)
}
fmt.Printf("LongGreet Response: %v", res)
}
サーバー
server構造体に、protocol buffersに定義されたGreetServiceServerと同じ形式で関数を埋め込む。
メッセージの送受信もprotocol buffersに定義されたものを用いている。また、リクエストの取得にも、メッセージに対するGet関数がprotocol bufferに用意されている。
package main
import (
"context"
"fmt"
"io"
"log"
"net"
"strconv"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/k-washi/example-golang-gRPC/streaming/greetpb"
"google.golang.org/grpc"
)
//server greet.pb.go GreetServiceClient
type server struct{}
func (*server) Greet(ctx context.Context, req *greetpb.GreetRequest) (*greetpb.GreetResponse, error) {
fmt.Printf("Greet func was invoked with %v", req)
firstName := req.GetGreeting().GetFirstName()
if firstName == "" {
return nil, status.Errorf(
codes.InvalidArgument,
"Recived a empty string",
)
}
if ctx.Err() == context.Canceled {
return nil, status.Error(codes.Canceled, "the client canceld the request")
}
result := "Hello " + firstName
//client config deadline
/*
res := &greetpb.GreetWithDeadlineResponse{
Result: result,
}
*/
res := &greetpb.GreetResponse{
Result: result,
}
return res, nil
}
func (*server) GreetManyTimes(req *greetpb.GreetManyTimesRequest, stream greetpb.GreetService_GreetManyTimesServer) error {
fmt.Printf("GreetManyTimes func was invoked with %v", req)
firstName := req.GetGreeting().GetFirstName()
for i := 0; i < 10; i++ {
result := "Hello" + firstName + " number " + strconv.Itoa(i)
res := &greetpb.GreetManyTimesResponse{
Result: result,
}
stream.Send(res)
time.Sleep(1000 * time.Millisecond)
}
return nil
}
func (*server) LongGreet(stream greetpb.GreetService_LongGreetServer) error {
fmt.Println("LongGreet function was invoked with a streaming request")
result := ""
for {
req, err := stream.Recv()
if err == io.EOF {
//we have finished reading the client stream
return stream.SendAndClose(&greetpb.LongGreetResponse{
Result: result,
})
}
if err != nil {
log.Fatalf("Error while reading client stream: %v", err)
}
firstName := req.GetGreeting().GetFirstName()
result += "Hello " + firstName + "! "
}
}
func main() {
fmt.Println("Hello World!")
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
greetpb.RegisterGreetServiceServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}