BETA

gRPC サーバーを Go で実装しローカルで動かす

投稿日:2019-05-20
最終更新:2019-05-23
※この記事は外部サイト(https://qiita.com/tetsuya/items/e09874cfc6...)からのクロス投稿です

go を使って gRPC サーバを立てローカルで動かすまでに必要となった知識をまとめた。

実装したコードは以下で公開している。

https://github.com/tetsuya/microservice-learning/

概要把握

RPC とは

Remote Procedure Call (RPC) に関しての説明は、以下のサイトがわかりやすかった。

「遠隔手続き呼び出し」とも呼ばれる。ネットワーク上に接続されたほかの端末のプログラムを呼び出し、実行させるための手法。または、そのためのプロトコルを指す。

同じRPCの仕様に準拠した仕組みを採用していれば、遠隔地にある端末でも、手元の端末からコマンドを入力して処理を実行できるのが特長。機種やOS、プログラミング言語などが異なっていても問題ない。

RPC | IT用語辞典 | 大塚商会

代表的な RPC の仕様に SOAPJSON-RPC などがある。

REST も RPC も HTTP 上で処理を実行し結果を受け取る仕組みだが、REST は Roy Fielding により提唱された HTTP メソッド (GET, POST, PUT, PATCH, DELETE) を活用した設計思想なのに対し、RPC は RFC 1831 として策定されている。

gRPC とは

Google が開発した HTTP/2 上で RPC を実現するフレームワーク。

Protocol Buffers とは

Protocol Buffers は IDL の一種である。また新しい略語が出てきたので引用しておく。

IDL(Interface Description Language、インタフェース記述言語)は、特定のプログラミング言語とは別にオブジェクトのインタフェースを指定するために使用される汎用言語です。

IDL (インタフェース記述言語) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

SOAP では WSDL(XML) で記述するが、gRPC では Protocol Buffers を利用する。

Protocol Buffers で定義されたメッセージは、バイナリ列や JSON などにシリアライズできる。公式ドキュメントでは XML と Protocol Buffers を比較し、構造がシンプルでファイルサイズが小さく、パースが早いことをメリットとして押し出している。

Why not just use XML? | Protocol Buffers

Protocol Compiler とは

C++ 製のコンパイラで、 Protocol Buffers (.proto) ファイルから、gRPC で処理をメッセージの受け渡しを行うためのコードを自動生成してくれる。protoc とも呼ばれる。

利用する際は、バックエンドの言語に合わせてプラグインとセットで使用する。よく、「gRPC は様々な言語に対応している」と言われるが、それが Protocol Compiler のプラグインが用意されているかに依存する。Go の場合、progo-gen-go がそれにあたる。

protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format

環境構築

gRPC と Protocol Buffers の大枠を把握したところで、まずは環境を構築していく。バックエンドの言語は Go を選択した。

Go Quick Start – gRPC を読みながら進めた。protoc は Homebrew で、 gRPC と protoc-gen-go は go get で入れた。

実装

最終的に Protocol Buffers は以下のような形になった。

syntax = "proto3";  

import "google/protobuf/timestamp.proto";  

service UserService {  
  rpc GetUser(GetUserRequest) returns (User) {}  
}  

message GetUserRequest {  
  int32 id = 1;  
}  

message User {  
  int32 id = 1;  
  string name = 2;  
  string email = 3;  

  google.protobuf.Timestamp updated_at = 15;  
}  

gRPC サービスの作成

Protocol Buffers の書き方は、Protocol Buffer Basics: Go | Protocol Buffers を参考に進めた。ただ、gRPC サービスの構成に関しては Defining the service – gRPC で触れられており、全体像はサンプルコード を参考に掴んでいった。

命名規則

RPC メソッドの命名に規則はないため、Google の API 設計ガイドで提唱されている標準メソッドを利用することにした。また、メソッド名、リクエスト/レスポンスメッセージも Google の命名規則に従うことにした。

動詞 名詞 メソッド名 リクエスト メッセージ レスポンス メッセージ
List Book ListBooks ListBooksRequest ListBooksResponse
Get Book GetBook GetBookRequest Book
Create Book CreateBook CreateBookRequest Book
Update Book UpdateBook UpdateBookRequest Book
Rename Book RenameBook RenameBookRequest RenameBookResponse
Delete Book DeleteBook DeleteBookRequest google.protobuf.Empty

命名規則 | Cloud API | Google Cloud

フィールド番号

proto ファイルを見ると、 = 1 と番号を振ってある。これはフィールド番号と呼ばれ、飛び番になっても構わないが、ユニークな番号を割り当てる必要がある。

1-15 の番号はフィールド番号と型を含めて、1バイトにエンコードされるが、16-2047の範囲は2バイトと、フィールドの番号が大きくなるとエンコード後のサイズが増加する。そのため、利用頻度の高いフィールドは 1-15 の番号を割り当てることが推奨されている。割り当てた番号は後から変えることを推奨されていないので、考えながら割り当てたい。

Assigning Field Numbers | Protocol Buffers

型のインポート

Protocol Buffers では独自の型を定義できる他に、公開されている型の定義ファイルをインポートできる。gRPC の go plugin には timestamp型が含まれおり意識せずに使えるようになっている。

一方、googleapis/googleapis などの公開定義ファイルの管理方法は様々なよう。ざっと調べたところ、以下の方法が見つかった。今のプロジェクトでは go mod での管理をすることにした。

  1. go のパッケージ管理ツール (dep or go mod) で管理
  2. 必要な定義ファイルだけをソースコードを同じように git 管理する
  3. stormcat24/protodep で管理する

インタフェースの生成

ここから、Protocol Compiler + プラグインを使い Go から呼び出せるインターフェースを生成した。

$ protoc --go_out=plugins=grpc:./protogen/ --proto_path=../../pb/ user.proto  

ディレクトリ構成は GoogleCloudPlatform/microservices-demo の shippingservice を参考にした。

サーバサイドの実装

Creating the server – gRPC を参考にしながら、サービスメソッドの実装を進めていった。

gRPC サーバの実装は Starging the server – gRPC を参考にしつつも、helloworld の実装を真似た。

結果的に、以下のような形になった。

package main  

import (  
  "context"  
  "log"  
  "net"  
  "time"  

  "github.com/golang/protobuf/ptypes"  
  pb "github.com/tetsuya/microservice-learning/src/userservice/protogen"  
  "google.golang.org/grpc"  
)  

const (  
  port = ":8080"  
)  

type server struct{}  

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {  
  log.Printf("Received: %v", req.Id)  
  // see: https://stackoverflow.com/a/52089674/1285818  
  now, _ := ptypes.TimestampProto(time.Now())  
  return &pb.User{Id: req.Id, Name: "John Smith", Email: "[email protected]", UpdatedAt: now}, nil  
}  

func main() {  
  lis, err := net.Listen("tcp", port)  
  if err != nil {  
    log.Fatalf("failed to listen: %v", err)  
  }  
  s := grpc.NewServer()  
  pb.RegisterUserServiceServer(s, &server{})  
  log.Printf("Listening on %v", port)  
  if err := s.Serve(lis); err != nil {  
    log.Fatalf("failed to serve: %v", err)  
  }  
}  

動作確認

gRPC サーバの起動

$ go run src/userservice/main.go  
2019/05/19 17:03:37 Listening on :8080  

利用可能なサービスの一覧を取得する

gRPC サーバに直接リクエストを送るために fullstorydev/grpcurl を利用した。

grpcurl -import-path pb/ -proto user.proto localhost:8080 list  
UserService  

リクエストの送信

期待通りのレスポンスを受け取ることができた。

$ grpcurl -plaintext -import-path pb/ -proto user.proto -d '{"id":"123"}' localhost:8080 UserService/GetUser  
{  
  "id": 123,  
  "name": "John Smith",  
  "email": "[email protected]",  
  "updatedAt": "2019-05-19T08:04:42.777350Z"  
}  

サーバ側のログも期待通りに出力されたことを確認した。

$ go run src/userservice/main.go  
...  
2019/05/19 17:04:42 Received: 123  

テスト

テスティングフレームワークは、Ginkgo を使うことにした。Ruby を使っていた時に Rspec を利用していた経験から書きっぷりに既視感があることと、CircleCI との連携を考えた時に JUnit 互換の XML が出力できることが採用の理由。

package main  

import (  
    "context"  

    . "github.com/onsi/ginkgo"  
    . "github.com/onsi/gomega"  
    pb "github.com/tetsuya/microservice-learning/src/userservice/protogen"  
)  

var _ = Describe("Main", func() {  
    Context("Request with total price", func() {  
        var (  
            resp *pb.User  
            err  error  
        )  

        BeforeEach(func() {  
            s := server{}  
            id := int32(123)  
            req := &pb.GetUserRequest{Id: id}  
            resp, err = s.GetUser(context.Background(), req)  
        })  

        It("will return no error", func() {  
            Expect(err).ShouldNot(HaveOccurred())  
        })  
        It("will return requested ID", func() {  
            Expect(resp.Id).To(Equal(int32(123)))  
        })  
        It("will return mocked name", func() {  
            Expect(resp.Name).To(Equal("John Smith"))  
        })  
        It("will return mocked email", func() {  
            Expect(resp.Email).To(Equal("[email protected]"))  
        })  
    })  
})  
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@tetsuyaの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!