何をするか?
ここでは、Go 言語用の gRPC ライブラリである gRPC-Go (google.golang.org/grpc) を使って、簡単な gRPC サーバーとクライアントを作ってみます。
通信用のスタブコードなどは、protoc
コマンド (Protocl Buffers Compiler) で .proto
ファイルから自動生成するので、あらかじめ protoc
コマンドをインストールしておいてください。
protoc
コマンドで Go 言語用のコードを生成するには、protoc-gen-go
プラグインと protoc-gen-go-grpc
プラグインをインストールしておく必要があります。
前者がシリアライズ用のコード、後者が gRPC 用のスタブコードを生成するための protoc
プラグインです。
# バージョンを指定してインストールする方法(推奨)
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
# 最新バージョンをインストールする方法
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
プロジェクトの作成と gRPC-Go のインストール
まずは Go 言語用のプロジェクトを作成します。
モジュール名は com.example/grpc-sample
としていますが、GitHub で管理する予定であれば、リポジトリ名に合わせて github.com/<USER>/grpc-sample
のような名前にしてください。
これが、プロジェクト内で作成する Go パッケージをインポートするときのプレフィックスになります。
$ mkdir grpc-sample && cd grpc-sample
$ go mod init com.example/grpc-sample
あとは、gRPC サーバーとクライアントの実装に使用する gRPC-Go パッケージの依存関係を追加しておきます。
$ go get google.golang.org/grpc
.proto ファイルを作成する
echo/echo.proto
ファイルを作成して、次のように記述します。
Go 言語用のオプション option go_package
で、出力する .go
ファイルを echo
パッケージに配置するように指定しています。
syntax = "proto3";
package echo;
option go_package = "example.com/grpc-sample/echo";
// Echo メソッドを持つ EchoService の定義
service EchoService {
rpc Echo (EchoRequest) returns (EchoResponse);
}
// Echo に送るリクエストメッセージの定義
message EchoRequest {
string message = 1;
}
// Echo が返すレスポンスメッセージの定義
message EchoResponse {
string message = 1;
}
ここでは、EchoService
というサービスが、Echo
というメソッドを提供するよう定義しています。
.proto ファイルをコンパイルする
protoc
コマンドを実行して、.proto
ファイルからシリアライズ用のコードと、gRPC 関連のスタブコードを生成します。
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
echo/echo.proto
それぞれのオプションは次のような意味があります。
--go_out
…protoc-gen-go
プラグインによる生成コードの出力先ディレクトリ--go_opt
…protoc-gen-go
プラグインに渡すオプション--go-grpc_out
…protoc-gen-go-grpc
プラグインによる生成コードの出力先ディレクトリ--go-grpc_opt
…protoc-gen-go-grpc
プラグインに渡すオプション
--go_out
オプションを指定することでシリアライズ用のコード (echo.pb.go
)、--go-grpc_out
オプションを指定することで gRPC 用のスタブコード (echo_grpc.pb.go
) を生成してくれます。
追加のオプションで、paths=source_relative
を指定することにより、入力ファイル (.proto
) と同じディレクトリ構成で .go
ファイルを出力するようにしています。
今回はカレントディレクトリ (.
) を出力のルートに指定しているので、結果的に入力ファイルと同じディレクトリに次のように .go
ファイルが生成されることになります。
入力ファイル | 使う protoc プラグイン | 生成されるファイル |
---|---|---|
echo/echo.proto |
protoc-gen-go |
echo/echo.pb.go |
echo/echo.proto |
protoc-gen-go-grpc |
echo/echo_grpc.pb.go |
生成された echo.pb.go
ファイルや echo_grpc.pb.go
ファイルを覗いてみると、次のようなパッケージ名で定義されていることがわかります。
package echo
このパッケージ名は、.proto
ファイル内の options go_package
で指定したパスに従って自動生成されています(パスの最後の /echo
という部分が採用されています)。
また、今回はモジュール名を example.com/grpc-sample
と定義したので(go.mod
ファイルに書かれているので)、自動生成されたこれらのパッケージをインポートするときは、次のような感じで記述することになります。
import "example.com/grpc-sample/echo"
gRPC サーバーとクライアントの実装
gRPC を使って通信するサーバーとクライアントは、それぞれ独立したコマンドとして cmd/echo-server
ディレクトリ、cmd/echo-client
ディレクトリ以下に作成することにします(それぞれ main
関数を作成します)。
Go 言語のプロジェクトで生成する実行ファイルのコードを cmd
ディレクトリ以下に配置するのはよくあるプラクティスです。
gRPC サーバーの実装
まずは、EchoService
を実装します。
protoc
によって自動生成された echo/echo_grpc.pb.go
の関数シグネチャを参考に、次のような感じで Echo
メソッドを実装します。
クライアントから受信したテキストの先頭に *
を付加したレスポンスを返しているだけなので実装は簡単です。
package main
import (
"context"
"log"
"example.com/grpc-sample/echo"
)
// EchoService を実装するサーバーの構造体
type server struct{
echo.UnimplementedEchoServiceServer
}
// EchoService の Echo メソッドの実装
func (s *server) Echo(ctx context.Context, in *echo.EchoRequest) (*echo.EchoResponse, error) {
log.Printf("Received from client: %v", in.GetMessage())
return &echo.EchoResponse{Message: "*" + in.GetMessage()}, nil
}
あとは、main
関数で gRPC サーバーのインスタンスを生成して、上記の実装を登録すれば OK です。
package main
import (
"fmt"
"log"
"net"
"google.golang.org/grpc"
"example.com/grpc-sample/echo"
)
const port = 52000
func main() {
// TCP ポートをオープンできるか確認
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// gRPC サーバーを生成し、EchoService サーバーの実装を登録する
s := grpc.NewServer()
echo.RegisterEchoServiceServer(s, &server{})
// gRPC サーバーを稼働開始
log.Printf("Server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
gRPC クライアントの実装
gRPC サーバー側が実装できたら、次はクライアント側の実装です。
下記の gRPC クライアントでは、Echo
メソッドを呼び出して AAAAA
というメッセージを送り、その応答を単純に出力しています。
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"example.com/grpc-sample/echo"
)
const addr = "localhost:52000"
func main() {
// EchoService サーバーへ接続する
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Did not connect: %v", err)
}
defer conn.Close()
c := echo.NewEchoServiceClient(conn)
// Echo メソッドを呼び出す
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.Echo(ctx, &echo.EchoRequest{Message: "AAAAA"})
if err != nil {
log.Fatalf("Could not echo: %v", err)
}
log.Printf("Received from server: %s", r.GetMessage())
}
実行してみる
まず、gRPC のサーバー側を起動します。
$ go run ./cmd/echo-server
2022/05/17 22:00:47 Server listening at [::]:52000
次に、gRPC のクライアント側を起動すると、サーバーと通信してメッセージを受信できていることを確認できます。
$ go run ./cmd/echo-client
2022/05/17 22:01:32 Received from server: *AAAAA
やったー!