株式会社Schoo 新卒1年目の @hiroto_0411です!
最近、業務でgRPCサーバーの実装を行うことになったのですが、gRPCに触れたことがなかったのでgRPCのサーバーを作りながら概要と実装方法をまとめてみました!
この記事でわかること
- gRPCってなに?
- gRPCサーバーを作る流れ
gRPCとは
Googleが開発したオープンソースのRPCフレームワーク
RPCとは
Remote Procedure Callの略。直訳すると遠隔手続き呼び出し。クライアントから別のサーバーの関数・メソッドにリクエストを送り、そのレスポンスを受け取ること。別のサーバーにある関数・メソッドを呼び出して使うようなイメージ。
REST APIと何が違うの?と思ったので調べてみた
RESTがリソース(データなどのサービスが提供しているもの)に対してHTTPメソッド(GET,POST,PUT,DELETEなど)で操作するという思想であるのに対し、RPCはメソッドを呼び出して特定の処理を行うという思想である。
この違いは、それぞれのAPIにリクエストを送るためのURIをみると分かりやすいかなと思った。
RPC
http://localhost:8080/registerUser
ユーザー登録の関数を呼び出している感じ。
REST
POST http://localhost:8080/users
usersというリソースに対してPOSTで登録している感じ。
思想以外にも、通信に使うプロトコルやデータの送受信方法などの違いはあるが、データの送受信を行うAPIを実装できるという点では同じである。
gRPCの特徴
- 送信するデータをProtocol Buffersというフォーマットを使用してバイナリデータに変換するため、軽量で高速なやりとりができる
- HTTP/2を利用した通信を行うことで、1つの接続で同時に複数のリクエストを処理できたり、効率の良いバイナリデータの転送ができる
- 1つのリクエストまたはレスポンスを分割して小さなチャンクで順次送受信するストリーミング通信ができる
gRPCサーバーを作ってみる
以下のQuick startを参考にしながらgRPCサーバーを作ってみた。今回はGoを使っていますが、流れは他の言語であっても大きくは変わらない。
作成したコードは以下で見られる。
ディレクトリ構成はこんな感じになる予定。
.
├── gen // helloworld.protoから自動生成される
│ ├── helloworld_grpc.pb.go
│ └── helloworld.pb.go
├── proto
│ └── helloworld.proto
├── server // 具体的な処理を書く。今回は{"message": "Hello Name"}を返すための実装を記載。
│ └── helloworld.go
├── buf.gen.yaml //.protoファイルからコードを自動生成するための設定を書く
├── buf.yaml
├── go.mod
├── go.sum
└── main.go
1. 準備
モジュールの作成
go mod init github.com/user-name/project-name
コード生成に必要なツールのインストール
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/bufbuild/buf/cmd/buf@latest
PATHを更新
export PATH="$PATH:$(go env GOPATH)/bin"
2. .protoファイルの作成
Protocol Buffersというフォーマットを使用してgRPCのサービス名やメソッド名、リクエストやレスポンスを定義する。
syntax = "proto3";
option go_package = "github.com/user-name/project-name/gen";
package hello;
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse){ }
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
この後、.protoファイルを元にコードを自動生成する。その時に出力したいpackage名をgo_package
で指定する。package hello;
はGoのpackage名とは関係ない。このpackageは名前が衝突するのを防ぐために使われる。
package user;
message Person {
string name = 1;
int32 age = 2;
}
package employee;
message Person {
string name = 1;
string employee_id = 2;
}
このように同じ名前のserviceやmessageを定義したときに問題が発生しないように、package名を指定する。
3. コードの自動生成
buf.yamlファイルをルートディレクトリに作成する
buf.yamlにはどのディレクトリの.protoファイルをコンパイルするかや、linterなどの設定、リモートのプロジェクトやサードパーティのProtobufファイルを参照するための依存関係の設定などを記述する。(今回は生成されたものをそのまま使う。)
buf config init
buf.gen.yamlをルートディレクトリに作成する
version: v2
plugins:
- local: protoc-gen-go
out: gen
opt:
- module=github.com/user-name/project-name/gen
- local: protoc-gen-go-grpc
out: gen
opt:
- module=github.com/user-name/project-name/gen
コードを自動生成する
以下のコマンドを実行することで、.protoファイルとbuf.gen.yamlを元に、XXX_grpc.pg.goとXXX.pb.goの2種類のファイルが自動生成される。
buf generate
XXX_grpc.pg.go
https://github.com/hiroto1220/go-playground/blob/main/gRPC/gen/helloworld.pb.go
サーバー(やクライアント)が.protoファイルのサービスに定義したメソッドを実装するためのインターフェースが含まれる。このインターフェースを使い、それぞれのサービスの実装を行う。
XXX.pb.go
https://github.com/hiroto1220/go-playground/blob/main/gRPC/gen/helloworld_grpc.pb.go
.proto ファイルで定義されたメッセージ・タイプと基本的なデータ構造のコードが含まれている。メソッドを呼び出したり実装したりする際には、これらの型を使用してリクエストオブジェクトとレスポンスオブジェクトを構築する。
4. 自動生成されたinterfaceを使い、サーバーを実装する
type GreeterServer interface {
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
mustEmbedUnimplementedGreeterServer()
}
type UnimplementedGreeterServer struct{}
func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
package server
import (
"context"
"log"
pb "github.com/user-name/project-name/gen"
)
type GreeterServer struct {
pb.UnimplementedGreeterServer
}
// ./gen/helloworld_grpc.pb.goに定義されているGreeterServerインターフェースを実装する
func (s *GreeterServer) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
log.Printf("Received: %v", in.GetName())
// 本来ならここから、別の層のメソッドを呼び出すなどする
return &pb.HelloResponse{Message: "Hello " + in.GetName()}, nil
}
./gen/helloworld_grpc.pb.goに定義されているGreeterServerインターフェースを実装し、それをmain.goで使用する。
package main
import (
"flag"
"fmt"
"log"
"net"
pb "github.com/user-name/project-name/gen"
"github.com/user-name/project-name/server"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
var (
port = flag.Int("port", 50051, "The server port")
)
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server.GreeterServer{})
log.Printf("server listening at %v", lis.Addr())
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
依存関係の整理
go mod tidy
5. サーバーにリクエストを送ってみる
サーバーの起動
go run main.go
grpcurlをインストール
brew install grpcurl
The main purpose for this tool is to invoke RPC methods on a gRPC server from the command-line. gRPC servers use a binary encoding on the wire (protocol buffers, or "protobufs" for short). So they are basically impossible to interact with using regular curl
gRPCurlはコマンドラインからgRPCサーバーを呼び出すツールである。curlはテキストベースのHTTPリクエストに対応しているが、バイナリ形式のProtocol Buffersには対応していない。また、ストリーミングにも対応していない。そのためコマンドラインからcurlで呼び出すことができない。(Connectを使う、grpc-gatewayを使うなど実装方法を変えるとcurlで通信できるようにもなる。)
リクエストを送信
grpcurl -plaintext -d '{"name": "パブりん"}' localhost:50051 hello.Greeter/SayHello
{
"message": "Hello パブりん"
}
grpc-gatewayの記事も書いているのでぜひご覧ください!
参考
Schooでは一緒に働く仲間を募集しています!