LoginSignup
16
12

More than 1 year has passed since last update.

知識ゼロから始める gRPC のサーバーサイド開発

Last updated at Posted at 2022-03-27

はじめに

スターティングgRPC という本を読んだので、そのアウトプット記事を書きます。
言語は Go です。

本を読む前の筆者のレベル感は以下であり、想定読者も同様になります。

  • Go は書いたことある
  • gRPC は 1mm もわからん

本記事では gRPC 未経験者が最低限のサーバーサイド開発をできるレベルに到達することを目標とします。

ベースは スターティングgRPC ですが、最低限の内容のみを抜き出しています。
特に「そもそも gRPC とはどいう技術か?」「既存技術と比較したときのメリット・デリットは?」「gRPC の採用判断はどのようにすべきか?」などは本の方にかなりわかりやすく書かれているため、ぜひ一読をおすすめします。

筆者は Docker でサクっと環境を作り学習しました。
学習時のソースコードは GitHub にアップロード しています。

本記事の範囲

本記事では gRPC とは何か?から始まり、実際の実装の流れまでを記述します。
実装では .proto ファイルを書き、コードを自動生成し、生成されたインターフェースを実装することになります。
最後に gPRC クライントでの動作確認を行います。

以下、本記事の範囲の詳細になります。

含むもの

  • gRPC とは何か?
  • gRPC の長所は何か?
  • .proto ファイルの書き方
  • gRPC サーバー実装

含まないもの

  • PRC とは何か?
  • REST との違いは何か?
  • gRPC の短所は何か?
  • gRPC が対応しているストリーミング形式
  • 双方向/双方向ストリーミングの実装方法

くどいようですが、上記はすべて スターティングgRPC に書いてあります。
本当におすすめです。

gRPC とは何か?

gRPC とは、以下の2つを実現する RPC フレームワークである。

  1. 高速な API 通信
  2. スキーマ駆動家初

現在、マイクロサービス間の内部通信の実現方法として有力視されている。

なぜ gRPC を使用するのか?

REST と比較し、2つの長所があるためである。

① HTTP/2 による高速通信

gRPC では HTTP/2 を採用しており、 HTTP/1.1 を採用する REST よりも高速である。
HTTP/2 ではデータ形式はバイナリであり、コネクションの接続・切断を都度行わなくて良い点が特徴である。

② スキーマ駆動開発による高生産性

gRPC では Protocol Buffers を採用している。
Protocol Buffers ではスキーマを定義し、コードを自動生成する。

スキーマ定義 → 自動生成の仕組みにより、API ドキュメントの管理が不要となる。
スキーマ = API ドキュメントとなるためである。

クラアントがサービスを利用したい場合、スキーマを見ればすべて書いてある。
バックエンドがサービスを追加・変更したい場合、スキーマを書けばコードを自動生成できる。

.proto ファイルの記述

Protocol Buffers では、スキーマは *.proto というファイルに独自の IDL で記述する。
実際の書き方は以下の記事で。

protoc によるコード生成

前提条件

Go を利用する場合、以下の3つのパッケージが必要である。

  • コンパイラ
    • protoc
  • コードジェネレータ
    • protoc-gen-go
    • protoc-gen-go-grpc

protoc のインストール

Mac の場合、以下でインストールできる。

$ brew install protobuf

コードジェネレータのインストール

Go をインストール済みの場合、以下でインストールできる。

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

生成コマンド

protoc コマンドでコードの自動生成が可能である。

$ protoc --go_out=path/to/output_dir --go-grpc_out=path/to/output_dir --go-grpc_opt=require_unimplemented_servers=false path/to/.proto_file

Go と gRPC のオプションを指定

Go と gRPC の2つのオプションを指定していることに注意。
Brotocol Buffers は何も Go 専門でもなければ、 gRPC 専門でもない。
そのため、生成先コードとして両方を指定する必要がある。

require_unimplemented_servers オプション

デフォルトで true である。

英語で調べても情報が出てこないので、正直あまり理解していない。
ざっくりインターフェースが実装されていない場合にエラーを返すメソッドと捉えている。
true を指定した場合は実装が求められるが、何を実装すればよいかイマイチわからない。

緊急性もなさそうなのでオプションで off にすることにより対応。

インターフェースの実装

ptoroc コマンドにより生成されたインターフェースを実装するコードを書く。

ハンドラー

基本的に型は自動生成されているため、行いたい処理を実装するだけでOKである。
コード例は後ほど。

ハンドラーの登録

ハンドラーを登録し、サーバーにエンドポイントを追加する。

server := grpc.NewServer()
api.RegisterXXXServiceServer(server, handler.NewXXXHandle())

server.Serve()

簡単なコード例

以下に簡単なコード例を示す。
パッケージ周りはザルであるが、勘弁願いたい。

実際に動くソースコードは GitHub に置いている。

.proto ファイル

user.proto
syntax = "proto3";
package user;

option go_package = "gen/api";

service UserGetService {
  rpc Get(UserRequest) returns (UserResponse) {}
}

message User {
  int32 id = 1;
  string name = 2;
}

message UserRequest {
  int32 id = 1;
}

message UserResponse {
  User user = 1;
}

ハンドラー

user_handler.go
package handler

import (
	"context"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"user/api/gen/api"
)

type userHandler struct{}

func NewUserHandle() *userHandler {
	return &userHandler{}
}

func (h *userHandler) Get(ctx context.Context, r *api.UserRequest) (*api.UserResponse, error) {
	if r.Id == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "ユーザーの ID を指定してください")
	}

	res := &api.UserResponse{
		User: &api.User{
			Id:   24,
			Name: "John",
		},
	}

	return res, nil
}

サーバー

server.go
package main

import (
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	"user/api/gen/api"
	"user/handler"
)

const port = 50052

func main() {
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	server := grpc.NewServer()
	api.RegisterUserGetServiceServer(server, handler.NewUserHandle())
	// To debug on grpc_cli.
	reflection.Register(server)

	go func() {
		log.Printf("start gRPC server port: %v", port)
		server.Serve(lis)
	}()

	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("stopping gRPC server...")
	server.GracefulStop()
}

gRPC クライアント

動作確認のために gRPC を叩きたくなります。

ただし、cURL ではレスポンスを可読することはできません。
Protocol Bufffers はデータ形式がバイナリだからです。

そのため、gRPC 専用のクライアントを使用する必要があります。
今回は grpc_cli を使用します。

エンドポイントを叩く

grpc_cli のインストール

Mac の場合は以下でインストールできる。

$ brew tap grpc/grpc

$ brew install grpc

叩いてみる

コードの実装例の場合、以下のコマンドでエンドポイントを叩ける。

$ grpc_cli call localhost:50052 user.UserGetService.Get 'id: 1'
connecting to localhost:50052
user {
  id: 24
  name: john
}
Rpc succeeded with OK status

スキーマ定義を見る

サーバーを reflect 可能にする

サーバーを reflection に登録します。

スキーマ定義はデフォルトでは外部呼び出しから見ることはできません。
本番運用では必要ない機能だからです。
この設定をすることで、 gRPC クライアントからスキーマ定義を参照可能になります。

import (
  ...
  "google.golang.org/grpc/reflection"
)

func main() {
  ...
  reflection.Register(server)
}

ls する

grpc_cli ls コマンドでスキーマ定義を見ることができます。
ファイルを見たほうが速いか、cli から見たほうが速いかは人によりそうですね。

$ grpc_cli ls localhost:50052 user.UserGetService -l
filename: user.proto
package: user;
service UserService {
  rpc Get(user.UserRequest) returns (user.UserResponse) {}
}

終わりに

モダンらしいとは聞きつつ中々手がつけられなかった gRPC の学習・解説をしました。
本では一部古くなっていた箇所も改修してコードにしています。

もし役に立ちましたら LGTM 押して頂けると励みになります。

参考文献

16
12
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
16
12