こんにちは。KOUKIです。
仕事で、gRPCという通信規格を使っているのですが、これが結構便利なので紹介したいと思います。
<目次>
Go言語記事まとめ
gRPCとは
gRPCは、Googleが開発したオープンソースのフレームワークで、異なる言語同士で実装されたアプリケーション間で通信を行えるようにできる便利なツールです。
例えば、Pythonで作られたシステムとJavaで作られたシステムがあるとします。
本来であれば、異なる言語で実装されたアプリケーションは言語実装が違うため、そのままでは直接やりとりを行うことはできません。
しかし、gRPCを導入すると言語規格を吸収し、相互のやりとりを可能にするための橋渡しを担ってくれます。
<イメージ>
Python App <———> gRPCのインターフェース <———> Java App
gRPCは、DockerやKubernetesのようなクラウドネイティブ基盤の一部にも使用されており、HTTP2の通信やストリーミングなどもサポートしているので、これからもガンガン使われていくのではないかと思います。
gRPCの始め方
gRPCを使うためにはあらかじめ、「message」と「services」を定義したprotoファイルを用意しておく必要があります。
gRPCは、Protocol Bufferとも呼ばれており、それらを定義するファイルには「.proto」拡張子を付けます。
protoファイルのサンプルを下記に記載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
syntax = "proto3" message Greeting { string first_name = 1; } message GreetRequest { Greeting greeting = 1; } message GreetResponse { string result = 1; } service GreetService { rpc Greet(GreetRequest) returns (GreetResponse) {}; } |
現時点では、上記のコードの意味は分からなくて問題ないです。コードを書きながら学んでいきましょう。
プロジェクトの準備
作業用ディレクトリ(プロジェクト)を準備します。
1 2 3 4 5 6 |
mkdir golang-grpc cd golang-grpc touch main.go touch generate.sh mkdir -p greet/greetpb touch greet/greetpb/greet.proto |
また、grpcのモジュールをインストールしましょう。
1 2 |
go get -u google.golang.org/grpc go get -u github.com/golang/protobuf/protoc-gen-go |
次にprotocをダウンロードします。
筆者は、Linuxを使っているので、Linux用のprotocをダウンロードしています。
それを適当なディレクトリに格納し、パスを通します。
筆者は、bashを使っているので、.bashrcに次の一文を書き込んで、パスを通しました。
1 |
export PATH=$PATH:/usr/local/protoc-3.10.0-linux-x86_64/bin |
.bashrcを変更したら忘れずに設定を有効にしましょう。
1 2 3 4 |
$ source ./.bashrc $ protoc --version libprotoc 3.10.0 |
protoファイルの作成
次にprotoファイルを作成しましょう。
実装場所は、greet.protoです。
1 2 3 4 5 6 7 8 9 10 11 |
// greet/greetpb/greet.proto // version syntax = "proto3"; // go pakcage package greet; // Include subdirectory into package too option go_package="greetpb"; service GreetService{}; |
protoファイルには、「GreetService」というサービスを定義しました。
このprotoファイルは、あるコマンドでコンパイルしてあげる必要があります。
generate.shに次のコードを記述してください。
1 2 3 4 5 |
#!/bin/bash # exportは環境によっては不要 export PATH=$PATH:/Users/user/go/bin/ protoc greet/greetpb/greet.proto --go_out=plugins=grpc:. |
このコードを実行してください。
1 2 3 4 5 6 7 8 |
./generate.sh # 実行できない場合 chmod 777 generate.sh # greet.pb.goができていることを確認 $ ls greet/greetpb/ greet.pb.go greet.proto |
注意) Missing input fileファイルになってしまった場合
シュルを実行した際、「Missing input file」が表示され、コンパイルに失敗しました。
原因は、GOPATHが設定されていなかったことにより、「go get -u github.com/golang/protobuf/protoc-gen-go」で取得したはずのprotoc-gen-goのバイナリファイルが意図しない場所にインストールされていたからでした。
その為、このエラーが出た場合は、GOPATHを設定してみてください。
GOPATHは、goのソースを取りまとめておく場所を指定する環境変数です。
この環境変数を「~/.bash_profile」に書き込んでおきましょう。
1 2 |
export GOPATH=$HOME/dev/go export PATH=$PATH:$GOPATH/bin |
以下のコマンドで、設定を有効にします。
1 |
source ~/.bash_profile |
設定が完了したら再度、「protoc-gen-go」をインストールしてみてください。
Serverコードの作成
gRPCでは、ServerコードとClientコードの作成を行います。
まずは、Serverコードから実装していきます。
下記のファイルを作成しましょう。
1 2 |
mkdir greet/greet_server touch greet/greet_server/server.go |
serverコードには、次の実装を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// greet/greet_server/server.go package main import ( "fmt" "log" "net" "github.com/selfnote/golang-grpc/greet/greetpb" "google.golang.org/grpc" ) type server struct{} 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) } } |
まず、ポート50051番のポート番号を設定して、リッスンの状態にします。
次に、grpcのNewServer関数にてサーバーを作成し、このサーバーに実行したい処理を登録します。
登録関数は、「RegisterGreetServiceServer」です。
これは、先ほど作成したgreet.pb.goファイルの中に設定されている関数です。
1 2 3 4 5 |
// greet.pb.goから抜粋 func RegisterGreetServiceServer(s *grpc.Server, srv GreetServiceServer) { s.RegisterService(&_GreetService_serviceDesc, srv) } |
RegisterGreetServiceServer関数の第二引数には、登録したいサービス名を指定するため、$server{}を渡しています。
これから「func (*server) GetCalc() int{ … }」みたいな形で、サービスを登録していきます。
Clientコードの作成
次に、Clientコードを作成します。
1 2 |
mkdir greet/greet_client touch greet/greet_client/client.go |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// greet/greet_client/client.go package main import ( "fmt" "log" "github.com/selfnote/golang-grpc/greet/greetpb" "google.golang.org/grpc" ) func main() { fmt.Println("Hello I'm a client") cc, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("could not connect: %v", err) } defer cc.Close() c := greetpb.NewGreetServiceClient(cc) fmt.Printf("Created client: %f", c) } |
Dial関数で、Client Connectionを作成します。
WithInsecureにてセキュアな通信を無効にし、ClientコードからServerコードに対して処理を実行します。
GreetAPI – protoファイルの作成 –
最初のサービスとして、GreetAPIを作成します。
gRPCの通信規格にはいくつか種類があり、今回は「gRPC Unary」を使っていきます。
gRPC Unaryは、一般的な一対一のRequest/Response通信です。
クライアントからサーバーへ単一のリクエストを送り、単一のレスポンスを受け取ります。
少量データの送受信に適している規格ですね。
それでは、protoファイルにgRPC Unaryとメッセージを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// greet/greetpb/greet.proto // version syntax = "proto3"; // go pakcage package greet; // Include subdirectory into package too option go_package="greetpb"; service GreetService{ // Unary rpc Greet(GreetRequest) returns (GreetResponse){}; }; message Greeting { string first_name = 1; string last_name = 2; } message GreetRequest { Greeting greeting = 1; } message GreetResponse { string result = 1; } |
各メッセージの数値は、ただの番号です。メッセージの塊ごとにを上から順に番号を振っていきます。
先頭のGreetingメッセージは、Go言語のStruct宣言みたいなものです。
1 2 3 4 5 6 7 8 |
// イメージ type Greeting struct { first_name string last_name string } var GreetRequest Greeting GreetRequest = Greeting{"Harry", "Potter"} |
設定が完了したらコンパイルしましょう。
1 |
./generate.sh |
GreetAPI – Serverコードの作成 –
Serverコードにサービスを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// greet/greet_server/server.go package main import ( "context" "fmt" "log" "net" "github.com/selfnote/golang-grpc/greet/greetpb" "google.golang.org/grpc" ) type server struct{} // new func (*server) Greet(ctx context.Context, req *greetpb.GreetRequest)(*greetpb.GreetResponse, error) { fmt.Printf("Greet function was invoked with %v\n", req) firstName := req.GetGreeting().GetFirstName() result := "Hello " + firstName res := &greetpb.GreetResponse { Result: result, } return res, nil } func main() { ... } |
Greet関数を新しく実装しました。
このGreet関数がどこから出てきたかというと「greet.pb.go」にinterfaceとして実装されています。
1 2 3 4 5 |
// GreetServiceServer is the server API for GreetService service. type GreetServiceServer interface { // Unary Greet(context.Context, *GreetRequest) (*GreetResponse, error) } |
このインターフェースを Server Structのメソッドとして実装したわけです。
GetFirstName関数もgreet.pb.goに実装されています。
1 2 3 4 5 6 |
func (m *Greeting) GetFirstName() string { if m != nil { return m.FirstName } return "" } |
protoファイルに定義されたメッセージの変数は、このようにgetterメソッドが定義されます。
GreetAPI – Clientコードの作成 –
続いて、Clientコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// greet/greet_client/client.go package main import ( "context" "fmt" "log" "github.com/selfnote/golang-grpc/greet/greetpb" "google.golang.org/grpc" ) func main() { fmt.Println("Hello I'm a client") cc, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("could not connect: %v", err) } defer cc.Close() c := greetpb.NewGreetServiceClient(cc) fmt.Printf("Created client: %f", c) // new doUnary(c) } func doUnary(c greetpb.GreetServiceClient) { fmt.Println("Starting to do a Unary RPC...") req := &greetpb.GreetRequest{ Greeting: &greetpb.Greeting{ FirstName: "Harry", LastName: "Potter", }, } res, err := c.Greet(context.Background(), req) if err != nil { log.Fatalf("error whiole caling Greet RPC: %v\n", err) } log.Printf("Response from Greet: %v\n", res.Result) } |
Clientコードのreq変数に格納している値は、protoファイルに定義したメッセージの内容そのものです。
greet.pb.goにそれぞれStructとして宣言されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type GreetRequest struct { Greeting *Greeting `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } type Greeting struct { FirstName string `protobuf:"bytes,1,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` LastName string `protobuf:"bytes,2,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } |
さて、いよいよ実行してみましょう。
その前に、main.goファイルと同階層にMakefileを作成してください。
1 |
touch Makefile |
ここに実行コマンドを加えます。
1 2 3 4 5 6 7 |
.PHONY: server client server: go run greet/greet_server/server.go client: go run greet/greet_client/client.go |
Makefileに実行したいコマンドを定義しておけば、コマンド実行がすごく楽になります。※それぞれのコマンド(go run)の先頭はtabでスペースを開ける必要があります
まずは、Serverコードを実行してみましょう。
ターミナルを二つ立ち上げて、片方のターミナル上で下記のコマンドを実行してください。
1 2 3 4 |
make server go run greet/greet_server/server.go Hello World |
Hello Worldが出力されたらOKです。
続いて、残りのターミナル上で下記のコマンドを実行してください。
1 2 3 4 5 6 |
make client go run greet/greet_client/client.go Hello I'm a client Created client: &{%!f(*grpc.ClientConn=&{0xc000060680 0x4d1ae0 localhost:50051 {passthrough localhost:50051} localhost:50051 {<nil> <nil> [] [] <nil> <nil> {{1000000000 1.6 0.2 120000000000}} false true 0 <nil> {grpc-go/1.25.0-dev 0x7dad40 false [] <nil> <nil> {0 0 false} <nil> 0 0 32768 32768 0 <nil>} [] <nil> 0xcb5f08 0 false true false <nil> <nil> <nil> <nil> 0x7dd800} 0xc00000eb80 {<nil> <nil> 0x7dad40 0 {passthrough localhost:50051}} 0xc000060640 {{0 0} 0 0 0 0} 0xc0000645a0 0xc000084550 map[0xc000091080:{}] {0 0 false} pick_first 0xc000060840 {<nil>} 0xc00000eb60 0 0xc0000266e0})}Starting to do a Unary RPC... 2019/10/24 15:23:16 Response from Greet: Hello Harry |
余計な情報も出力されているため分かりずらいですが、「Response from Greet: Hello Harry」が出力されました。
Serverとの通信が正常に行われたようです。
Serverコードを実行しているterminal上でも以下の出力が確認できます。
1 |
Greet function was invoked with greeting:<first_name:"Harry" last_name:"Potter" > |
長くなってきたので、今日はここまでにしましょう。
次回
次回は、Server Streaming APIを使ったAPIを作成します。
コメントを残す
コメントを投稿するにはログインしてください。