モバイルエンジニアとして、CBcloudに参画したbigbackboomです。メインでAndroidアプリの開発を行なっています。今回Adventカレンダー三度目の投稿です!!!
さてさて、また会社で使ってないGoかよと自分でも思いますが、これには訳があるんです。そもそも、これからの戦略を考えて、gRPCを使えるようになろうと思っていた次第なのですが、問題が・・・
通信ってサーバーとクライアント両方ないと成り立たないんですよね・・・
当然といえば、当然ですが、両方ないとお勉強もできないので、まずは私が好きな言語でサーバーから作っちゃおと言う感じですね。
gRPCとは?
gRPC Remote Procedure Call とはGoogleが開発したRPCの一つです。私も世代的には通信はREST通信が主流でRPCはピンと来ないのですが、Wikipediaでは以下のように書かれています。
プログラムから別のアドレス空間(通常、共有ネットワーク上の別のコンピュータ上)にあるサブルーチンや手続きを実行することを可能にする技術
これでもピンときませんな!
詰まるところ、RESTは **ユニークなリソース(エンドポイント)に対して、よく定義された操作(GET/PUT/POST/DELETE)で、通信情報には全ての状態を定義した(逆にクライアントには状態定義はせず→ステートレスクライアント)**通信をRESTと言うみたいです。まぁ私も、そこまで詳しい訳じゃないので、あまり適当なことを言うとREST警察に刺されるかもしれないのでこれぐらいで。
それと比べて、RPCは遠隔に用意された動作を呼び出す。例えばUserの一覧が欲しければ、listUser()
などの操作を呼び出す訳です。この考えをもとに作られたプロトコルが、SOAPだったり、JSON-RPCとなるみたいですね。
そのRPCの考えをもとに、HTTP/2の利点を最大限に生かせるようにしたのがgRPCのようです。
gRPC でできること
gRPCでは以下のような通信を実現することができます:
- Unary RPC (1リクエスト1レスポンス)
- Server streaming RPC (1つのリクエストに複数レスポンス)
- Client streaming RPC (複数のリクエストに一つのレスポンス)
- Bidirectional streaming RPC(双方向)
Protocol Bufferファイル
gRPCではAPIのインターフェース記述にProtocol Bufferを利用している。全てを説明していたらキリがないのでざっくりと言いますと、Protocol BufferはGoogleが開発した、インターフェース記述言語でデータ量とパースの速度とシリアライズが高速になることに主眼が置かれています。
gRPCではこのファイルをまず定義して、コンパイルすることで、指定したプラットフォーム(プログラミング言語)のサーバーやクライアントで必要なクラスなどを自動生成することができる訳です。
やってみよう
gRPCのインストール
go get -u google.golang.org/grpc
protocのインストール
protocのダウンロードして、インストールしてください。
インストーラーなどないのでそこそこ面倒です。自分のOSに合わせて頑張ってください!
とはいえ、PATHの通ったディレクトリに解凍したディレクトリの/bin の中のバイナリを
移すだけですが。
protocのGo用のプラグインをインストール
$ go get -u github.com/golang/protobuf/protoc-gen-go
Protocol Buffer
protocファイルは以下のように記述。
意味合いとしては、GetEcho
という動作をGetEchoMessage
のパラメータで呼び出せば、EchoResponse
で帰ってくるというふうになります。えらくわかりやすいなぁ。さすがGoogle
syntax = "proto3";
service Echo {
rpc GetEcho (GetEchoMessage) returns (EchoResponse) {}
}
message GetEchoMessage {
string target_echo = 1;
}
message EchoResponse {
string input = 1;
}
messageがレスポンスとリクエストの値になる訳ですね。イコールの後にある数字はタグのようで、メッセージごとに一意であれば問題ないようです。タグが数字な理由としては、リファクタしたら、パラメータ名が変わってしまったという場合でも問題なく動くようにするためらしいです。
protoファイルからコードの生成
protoc --go_out=plugins=grpc:. service/*.proto
protocがコマンドで、go言語で、アウトプットを出す場合go_out=plugin=grpc:ディレクトリ名
を指定します。絶対にpluginの指定も忘れないように。これを忘れるとgrpc用のserver/clientのgo interfaceが生成されません。
その後ろに、protoファイルが存在する場所を指定しています。 serviceディレクトリ直下に置いているのでservice/*.proto
とコマンド入力をしている訳です。ちなみに、protoファイルを分割して、別のprotoファイルからimportするなどもでき、その場合は-I
オプションでimportするファイルのパスを指定などもあります。
上記のコマンドを入力すると、*.pb.go
というファイルが生成されます!
実装
サーバーロジック
ではロジックを書いてみましょう。とはいえ、echo
という名前の通りやまびこなので、もらったパラメーターをそのまま返しましょう。この意味のない循環の美しさ素晴らしい。
import (
"context"
)
type EchoService struct {
}
func (s *EchoService) GetEcho(ctx context.Context, message *GetEchoMessage) (*EchoResponse, error) {
return &EchoResponse{
Input: message.TargetEcho,
}, nil
}
GetEcho
はpb.goファイルを見ると、サーバー側のインターフェースとして実装されています。
サーバー起動
ポートは😁ニコニコ😁
// server.go
import (
"google.golang.org/grpc"
"log"
"net"
service "grpcTest/service"
)
func main() {
listenPort, err := net.Listen("tcp", ":2525")
if err != nil {
log.Fatalln(err)
}
server := grpc.NewServer()
echoService := &service.EchoService{}
service.RegisterEchoServer(server, echoService)
err = server.Serve(listenPort)
if err != nil {
log.Fatalln(err)
}
}
クライアントサイド
// client.go
import (
"context"
"fmt"
"google.golang.org/grpc"
"log"
service "grpcTest/service"
)
func main() {
//sampleなのでwithInsecure
conn, err := grpc.Dial("127.0.0.1:2525", grpc.WithInsecure())
if err != nil {
log.Fatal("client connection error:", err)
}
defer conn.Close()
client := service.NewEchoClient(conn)
message := &service.GetEchoMessage{TargetEcho: "bigbackboom"}
res, err := client.GetEcho(context.TODO(), message)
fmt.Printf("result:%#v \n", res)
fmt.Printf("error::%#v \n", err)
}
実行
ターミナルを二つ起動します。
まずサーバーから以下のコマンドで手っ取り早く、実行
go run server.go
次にクライアントを実行。問題なければResponseが帰ってくる。
go run client.go
# 実行結果
# result:&echoService.EchoResponse{Input:"bigbackboom", XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
# error::<nil>
返ってきた!!!!
まとめ
かなり急ぎで、とりあえずgoのgRPCを3時間ほどでミニマム実装をしてみましたが、サーバーサイド・クライアントサイド両方のインターフェースが自動生成されるのがかなり好感度高い。
一度、protocファイルを作れば、そのサーバーを使う全く別の言語でも使いまわせるので、言語ごとの変な仕様に惑わされることもないですし。
とりあえず今回はここまでですが、Androidクライアントでも試したいのと(本業はそっちですし)、他にも双方向バインディングやら色々試してみたいので、ここで作った物を踏み台にしていきたいです!!!
ちなみにソースはこちら