gRPCってなに？サーバーを作りながら理解してみよう

株式会社Schoo 新卒1年目の [@hiroto_0411](https://x.com/hiroto_0411)です！
最近、業務でgRPCサーバーの実装を行うことになったのですが、gRPCに触れたことがなかったのでgRPCのサーバーを作りながら概要と実装方法をまとめてみました！

## この記事でわかること
- gRPCってなに？
- gRPCサーバーを作る流れ


## gRPCとは
Googleが開発したオープンソースのRPCフレームワーク

### RPCとは
Remote Procedure Callの略。直訳すると遠隔手続き呼び出し。クライアントから別のサーバーの関数・メソッドにリクエストを送り、そのレスポンスを受け取ること。別のサーバーにある関数・メソッドを呼び出して使うようなイメージ。

gRPCのイメージ
![スクリーンショット 2024-10-17 0.44.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3810533/d405e460-3c66-6870-4511-fc3fbef1db0e.png)

https://grpc.io/docs/what-is-grpc/introduction/


### 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を使っていますが、流れは他の言語であっても大きくは変わらない。

https://grpc.io/docs/languages/go/quickstart/

作成したコードは以下で見られる。

https://github.com/hiroto1220/go-playground/tree/main/gRPC

ディレクトリ構成はこんな感じになる予定。
```
.
├── 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](https://protobuf.dev/overview/)というフォーマットを使用してgRPCのサービス名やメソッド名、リクエストやレスポンスを定義する。

~~~./proto/helloworld.proto
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
```

<br/>

**buf.gen.yamlをルートディレクトリに作成する**
~~~./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
~~~

| 設定項目 | 説明 |
|----------|---------------------------------------------------------------|
| `version: v2` | Buf のバージョンを指定（v2 は最新のフォーマット） |
| `plugins` | `.proto` をコンパイルするときに使用するプラグインを指定 |
| `local: protoc-gen-go` | `protoc-gen-go`（Go 用の Protocol Buffers 生成ツール）をローカルで使う |
| `out: gen` | 生成されたコードを `gen/` ディレクトリに出力する |
| `opt: module=github.com/user-name/project-name/gen` | 生成された Go パッケージのモジュールパスを指定する（`go.mod` の `module` と一致させる） |
| `local: protoc-gen-go-grpc` | `protoc-gen-go-grpc`（gRPC 用のコードを生成するプラグイン）をローカルで使う |



<br />

**コードを自動生成する**
以下のコマンドを実行することで、.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を使い、サーバーを実装する
~~~./gen/helloworld_grpc.pb.go
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() {}
~~~

~~~./server/helloworld.go
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](https://github.com/hiroto1220/go-playground/blob/main/gRPC/gen/helloworld_grpc.pb.go)に定義されているGreeterServerインターフェースを実装し、それをmain.goで使用する。

~~~./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
```

https://github.com/fullstorydev/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](https://connectrpc.com/docs/introduction)を使う、[grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway)を使うなど実装方法を変えるとcurlで通信できるようにもなる。)

**リクエストを送信**
```
 grpcurl -plaintext -d '{"name": "パブりん"}' localhost:50051 hello.Greeter/SayHello
```
```
{
  "message": "Hello パブりん"
}

```

---
grpc-gatewayの記事も書いているのでぜひご覧ください！

https://qiita.com/hiroto_0411/items/f72d8e87a18a4c6ce5c0



## 参考

https://qiita.com/S4nTo/items/0ff0445542538ef49a05

https://miraitranslate-tech.hatenablog.jp/entry/rest-and-roa#ROA%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E6%8C%87%E5%90%91%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

https://protobuf.dev/overview/

https://grpc.io/docs/what-is-grpc/introduction/

---
**Schooでは一緒に働く仲間を募集しています！**

https://corp.schoo.jp/careers/graduates

https://corp.schoo.jp/careers