自分の中で少しずつ理解できてきたので、小分けにして整理していきたくこの記事を書きました。
同じ初学者のお役に立てれば。
当記事は実装メインで解説します。
protoって何の略?これは内部でどうなってるの?的な細かい話は省いています。
気になるところがあったら他記事で補完してください🙏
構成
今回は最小限の超簡単な構成とします。
初学者はHello,World!を出力しなければならないという法律に則り、Hello,World!を出力するAPIを実装していきます。
実装では、クライアントのインスタンス、gRPCサーバのインスタンスをそれぞれ立ち上げ、リクエスト〜レスポンスまでの流れを確認する、ということをやります。
大まかな手順
ゆるふわ概念図を記しておきます。
全体像
まずは作成するものの全体像。さっきのクライアント↔︎gRPCサーバの図をもうちょい細かく︎書いた図です。
※クライアント(インスタンス)、gRPCサーバ(インスタンス)はgoファイルをコマンド実行している間だけ生きています。一方、pb.goファイルは一度作成されたらずっとそこにあります。
ざっくり手順 〜3ステップ〜
上の図より、初手で作るファイルは以下の3つになります。
- server.go // サーバの実装
- client.go // クライアント側の実装
- hello.proto // APIの細かいリクエスト・レスポンス内容の実装
フォルダ構成
1つのフォルダ直下に全てをぶっ込みます。フォルダ構成が簡単だとテンション上がるので。
grpc-test/
┝ server.go
┝ client.go
└ hello.proto
いざ実装!の前に
GitHubに空のリポジトリ「grpc-test」を作っておき、
このリポジトリをローカルに持ってきた上で、
「grpc-test」直下に諸々のファイルを作っていく
という流れをやっておきます。
(pb.goファイルのURL指定のため)
hello.protoの実装
まずは第一にこれを実装します。なぜならこのprotoファイルにリクエスト・レスポンスの全てをぶち詰め込むからです。goでのgrpcはこの書き方が一般的です。
syntax = "proto3";
package hello;
// このファイルにより生成されるpb.goファイルを置く(出力する)ディレクトリ
option go_package = "/pb";
service HelloService {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
コード中
option go_package = "/pb";
の部分は、pb.goファイルが生成される場所になります。
今回、上記のように指定したことで、grpc-test直下にpbファイルが作られることが確定しました。他のパスに置きたければここを編集すればOK。
次の項「pb.goファイルの生成」を実行したあとは、こんな配置になるはず。
grpc-test/
┝ pb/
| ┝ hello.pb.go
| └ hello_grpc.pb.go
(略)
pb.goファイルの生成
- ターミナルを開き、今回のルート「grpc-test」フォルダ直下に移動します。
- 以下のコマンドを実行します。
protoc --go_out=. --go-grpc_out=. hello.proto
- 「pb」フォルダが作成され、直下に「hello.pb.go」「hello_grpc.pb.go」が作成されています。
現時点でのフォルダ構成は以下のようになっていると思います。
grpc-test/
┝ pb/
| ┝ hello.pb.go
| └ hello_grpc.pb.go
┝ server.go
┝ client.go
└ hello.proto
protoファイル作成、pb.goファイル作成が完了しました。
ここで先ほどの図を振り返ってみます。今回やったのは↓の部分にあたります。
server.go の実装
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/<your_user_name>/grpc-test/pb" // 後ほど生成されたhello.pb.goとhello_grpc.pb.goが置いてあるパスに変更
)
type server struct {
pb.UnimplementedHelloServiceServer
}
// このSayHelloメソッドがサービスハンドラーの役割を果たす
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051") // リスナー作成
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer() // gRPCサーバーのインスタンスを作成
pb.RegisterHelloServiceServer(s, &server{}) // HelloServiceサービスをサーバーに登録
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
ここで
go mod init
go mod tidy
コマンドを実行して、go.mod
、go.sum
兄弟をつくっておきます。
go run server.go
コマンドを打つとgRPCサーバがつくられます。
start listening at で返されればOK。ctrl+Cで抜けれます。
(別に読まなくていい補足)
RPCではHTTPルーティングのようなURLベースのルーティングはなく、
protoファイルで定義されたサービスとメソッドがルーティングの役割を果たします。
今回は、SayHelloメソッドがHelloServiceサービスにルーティングされるようになります。
client.goの実装
package main
import (
"context"
"log"
"os"
"time"
"google.golang.org/grpc"
pb "xxx" // 生成されたhello.pb.goとhello_grpc.pb.goのパッケージパスに変更
)
func main() {
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewHelloServiceClient(conn)
name := "world"
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.GetMessage())
}
ここで
go mod tidy
コマンドを実行して、go.mod
、go.sum
を更新しておきます。
先ほどの図では↓の部分にあたります。
リクエスト送信、レスポンス受け取り&表示までが、クライアント側で実装した内容になります。
main関数 func main()
が同じ階層に2つ以上存在する場合、警告が出るっぽいです。
mainの実行自体はできるので、今回は警告を無視して進めます。
(別に読まなくていい補足)
この実装したクライアントは、リモートプロシージャコール(RPC)という通信方法でリクエストを送信します。
先ほど作成したserver.goのサーバも、リモートプロシージャコール(RPC)という通信を受け取り、処理します。
クライアントからリクエストをgRPCサーバに投げてみる
ここまでの実装で、やっと完成です。
フォルダはこんな感じになってるはず。
grpc-test/
┝ pb/
| ┝ hello.pb.go
| └ hello_grpc.pb.go
┝ go.mod
┝ go.sum
┝ server.go
┝ client.go
└ hello.proto
それでは、早速やってみます。
順序はこれだけ。
- gRPCサーバを立ち上げる
- クライアントからリクエストを投げる
1. gRPCサーバを立ち上げる
「grpc-test」リポジトリ直下に行きます。
ターミナルを立ち上げ、以下のコマンドを実行します。
go run server.go
これで start listening at <port番号>
のような形で返されればOKです。
ctrl+Cで抜けれます。
2. クライアントからリクエストを投げる
「grpc-test」リポジトリ直下に行きます。
先ほどサーバを起動したターミナルとは別のターミナルを立ち上げ、以下のコマンドを実行します。
go run client.go
これで Greeting: Hello world
と返されました!
ちなみに、
go run client.go hoge
を実行すると、Greeting: Hello hoge
と返されます。
gRPC APIのつくりかた まとめ
protoファイルを作ってpb.goを生成する。そのあと、サーバ、クライアント側の実装をする。
これでファイルは完成。
次に、go run server.go
コマンドでgRPCサーバを作成。
最後に、クライアントのインスタンスを生成し、gRPC APIを叩く。
すると、以下のようなルートを通り、レスポンスが返されます。
リクエスト、レスポンスの中身はgRPCサーバ内でいろいろと加工されたりします。
厳密には違う部分もあるかもですが、ざっくりこんな感じです。
ご指摘あれば何なりと!