LoginSignup
2
2

More than 3 years have passed since last update.

Go言語でgRPCの門を叩いたお話

Posted at

gRPCとは

Google様が創造したRPCフレームワークです。
データをシリアライズして、高速にデータを通信できます。
通信部分にはHTTP/2を利用しています。
Protoファイルを作成すればそこから実装に使いたい言語のファイルを生成できるので、異なる言語で実装されているシステム間のやり取りにも使えます。

対応言語

  • C++
  • C#
  • Dart
  • go
  • Java
  • Kotlin
  • Node.js
  • Ruby
  • Python
  • PHP
  • Objective-C

RPCとは

Remote Procedure Call の略で、ローカルのメソッドを呼び出すようにサーバー側のメソッドを呼び出せるものです。

結局なんなの?

他のシステムのメソッドを爆速通信で呼び出せる技術。

環境

OS:macOS Catalina 10.15.7
Go:1.14.7

準備

  • gRPCインストール
go get -u google.golang.org/grpc
  • Protocol Buffers v3 インストール
brew install protobuf
  • Go用protocプラグインインストール
go get -u github.com/golang/protobuf/protoc-gen-go

protoファイルの書き方

ここからはprotoファイルの書き方を見ていきます。

version

これを記述しないとprotocol buffer 2と解釈されます。
現在の主流はバージョン3なので、基本的にはそれに従ってバージョン3を使うといいと思います。

syntax = "proto3";

package

Packageを定義することで他のprotoファイルとのmessage(後述)の名前衝突を避けることができる。
またGoファイルに変換する場合、option go_package を使わない限りこの名前がそのままpackage名になる。

package sample;

message

gRPCを使ってやりとりするデータの型をこのmessageで定義する。
下記は定義例です。

message PersonRequest {
  string name = 1;
  int32 age = 2;
}

例とを見れば大体書き方はわかるかと思いますが、各フィールドについている数字について説明します。
このタグナンバーはデータをバイナリ変換する時にフィールドを区別するために使われるものです。
よく使われるフィールドは小さい番号で設定するといいと思います。
ただし注意点としてタグナンバーの振り直しは非推奨なので、変更する場合は新たにタグナンバーを割り振ってください。

フィールドに指定するデータ型には他のメッセージ型を指定したり、頭に repeated をつけて配列にすることもできる。

service

gRPCで使う関数をserviceとrpcで定義する。
下記は定義例です。

service PersonService {
    rpc AddIsAdult (PersonRequest) returns (PersonResponse);
}

Goファイルを生成すると、service の名前がそのままinterfaceの名前となります。
rpc の名前は関数名になり、引数として使うmessage型と戻り値として使うmessage型を定義します。

実装

それでは実際にgRPCを使ってみたいと思います。
今回は簡易的に実装するのでサーバー側もクライアント側もmain.goに実装してしまいます。ご容赦を。
github: https://github.com/jgvkmea/grpc-entry

ディレクトリ構造

.
├── client
│   └── main.go
├── pb
│   ├── sample.pb.go
│   └── sample.proto
└── server
    └── main.go

protoファイル

上の書き方にならってprotoファイルを作成しました。
今回は名前と年齢を引数として渡して、それを使って挨拶メッセージを返す関数を実装します。

pb/sample.proto
syntax = "proto3";

package sample;

option go_package = "./pb";

message HelloPersonMessageRequest {
  string name = 1;
  uint32 age = 2;
}

message HelloPersonMessageResponse {
  string hello_message = 1;
}

service Hello {
  rpc HelloPerson (HelloPersonMessageRequest) returns (HelloPersonMessageResponse);
}

Goファイル生成

作ったprotoファイルをもとにGoファイルを生成します。

protoc --go_out=plugins=grpc:<出力先パス> <protoファイルパス>

のようにコマンドを実行すればいいので、今回は下記のようにコマンドを実行しました。

protoc --go_out=plugins=grpc:. ./pb/sample.proto

コマンドを実行すると pb ディレクトリ内に sample.pb.go が生成されます。
このファイル内の以下を使ってserver側とclient側をそれぞれ実装していきます。

server実装

server側の実装をします。

  • serverインターフェースを実装した構造体実装
  • メソッドの処理内容実装
  • gRPC serverの登録・起動

serverインターフェースを実装したserver構造体実装

今回生成されたインターフェースはこちらです。

sample.pb.go
// HelloServer is the server API for Hello service.
type HelloServer interface {
    HelloPerson(context.Context, *HelloPersonMessageRequest) (*HelloPersonMessageResponse, error)
}

このインターフェースを実装したserverがこちらです。

server/main.go
// HelloServer serverの構造体
type HelloServer struct{}

// HelloPerson HelloPersonのメソッド
func (h *HelloServer) HelloPerson(ctx context.Context, req *pb.HelloPersonMessageRequest) (res *pb.HelloPersonMessageResponse, e error) {
    return res, nil
}

メソッドの処理内容実装

server構造体を作成するときに定義したメソッドに処理内容を実装します。
今回は名前と年齢を受け取って挨拶文を返す処理を実装します。

server/main.go
// HelloServer serverの構造体
type HelloServer struct{}

// HelloPerson HelloPersonのメソッド
func (h *HelloServer) HelloPerson(ctx context.Context, req *pb.HelloPersonMessageRequest) (res *pb.HelloPersonMessageResponse, e error) {
    text := fmt.Sprintf("%sさん(%d歳)、こんにちは。", req.Name, req.Age)
    res = &pb.HelloPersonMessageResponse{HelloMessage: text}
    return res, nil
}

レスポンスはprotoで定義したmessage型に合わせて .pb.go ファイルに構造体が定義されているので、その構造体に値を入れて返せばOKです。
エラーについては errors.New で返したり、status.New からerrorに変換して返したりすればいいと思います。
statusの参考記事:https://qiita.com/Hiraku/items/89899bfc67abfed1fe6c

gRPC serverの登録・起動

作成したserver構造体を生成してgRPCのserverに登録し、起動します。

server/main.go
func main() {
    listenPort, err := net.Listen("tcp", ":19003")
    if err != nil {
        log.Fatalln(err)
    }
    // サーバー登録
    s := grpc.NewServer()
    pb.RegisterHelloServer(s, &HelloServer{})
    s.Serve(listenPort)
}

サーバーの登録は .pb.gp ファイルに RegisterXXXServer() という関数が定義されているのでそれを使って実装してください。

client実装

今度はクライアント側の実装をしていきます。
やることとしてはserverへのコネクションを作成し、server側のメソッドを呼び出すだけです。

client/main.go
func main() {
    // コネクション作成
    conn, err := grpc.Dial("localhost:19003", grpc.WithInsecure())
    if err != nil {
        log.Fatalln(err)
    }
    defer conn.Close()
    client := pb.NewHelloClient(conn)

    // メソッド呼び出し
    req := &pb.HelloPersonMessageRequest{
        Name: "Qiita",
        Age:  9,
    }
    res, err := client.HelloPerson(context.TODO(), req)
    fmt.Println(res.HelloMessage)
}

(Qiitaはサービス開始が2011年9月16日らしいです。)

メソッドの引数は .pb.go ファイルにリクエスト用の構造体が定義されているので、それを使って渡してください。

実行

サーバー側を実行した上でクライアント側を実行してみます。

server実行

$ go run server/main.go

server実行中の状態でclient側を実行します。

$ go run client/main.go
Qiitaさん(9歳)、こんにちは。

ちゃんと挨拶コメントが返ってきましたね。

おわりに

今回はGo言語を使ってgRPCへの入門として簡単に実装してみました。
次はgRPC-gatewayを使ってgRPCを使ったAPIを作ってみたいと思います。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2