grpc-gatewayでRESTful API/gRPC API実装

# RESTful API/gRPC API提供

gRPC-GatewayはgRPCで提供されているAPIをRESTful　APIに変換して、提供するためのコード生成ができます。
なので、HTTP/2をサポートしていない環境からは、RESTful　API経由で、リクエストを受け付けるといった使い方ができます。

*.protoファイルで一括管理できて、サーバーの管理がとても楽になります。

備忘録です。

## 環境

```
% go version 
go version go1.22.5 darwin/arm64
```

```
% protoc --version
libprotoc 3.20.3
```

### 実行ファイルをインストール

```
go install \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
```

### version

```
% protoc-gen-grpc-gateway --version
Version v2.22.0, commit unknown, built at unknown
```

```
% protoc-gen-openapiv2 --version
Version v2.22.0, commit unknown, built at unknown
```

※`echo ${GOPATH}/bin`などにパスは通しておく。

## 概要

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/443312/a65ee1e2-f299-8cf6-3554-08c63d3134da.png" width="600">

引用：
https://grpc-ecosystem.github.io/grpc-gateway/#getting-started

## Write and download Proto files

図書館の本を借りるサービスをGolangで実装します。このサービスにRestful APIとgRPC APIを実装します。

```
mkdir sample-book-lending
```

```
go mod init sample-book-lending
```

protofileを格納するディレクトリです。
```
mkdir proto
cd proto
```

### Copy `google/api/annotations.proto` and `google/api/http.proto`

Copy `google/api/annotations.proto` and `google/api/http.proto`
Proxy Serverの実装のためのProtofileをコピーします。
```
mkdir -p google/api
cd google/api
```

```
curl -O https://raw.githubusercontent.com/googleapis/googleapis/refs/heads/master/google/api/annotations.proto
curl -O https://raw.githubusercontent.com/googleapis/googleapis/refs/heads/master/google/api/http.proto
```

```
mv *.proto google/api/
```

### *.proto作成

book.proto
```
syntax = "proto3";

option go_package = "./pkg/grpc";

message Book {
  string title = 1; //
}
```

account.proto
```
syntax = "proto3";
// for proxy
import "google/api/annotations.proto";
import "book.proto";

option go_package = "./pkg/grpc";

package myapp;

// 本を借りるためのAccount
message Account {
  string name = 1;
}

message AccountInfo {
  string name = 1;
}

message BorrowRequest {
  Account account = 1;
  Book book = 2;
}

message BorrrowResponse {
  Account account = 1;
  Book book = 2;
}


// サービスの定義
service LendingBooksService {
  
  rpc SendBorrow (BorrowRequest) returns (BorrrowResponse);
  
  rpc AccountInfo (Account) returns (Account) {
    option (google.api.http) = {
      post: "/v1/accountinfo/books"
      body: "*"
    };
  }
}

```

```
pwd
sample-book-lending/proto
```

```
proto % tree
.
├── account.proto
├── book.proto
└── google
    └── api
        ├── annotations.proto
        └── http.proto
```

### *.protoをビルド

protocを実行します。※bufを使用する方法もありますが今回はprotocを使用します。
```
protoc -I ./proto \
--go_out=./pkg/grpc --go_opt paths=source_relative \
--go-grpc_out ./pkg/grpc --go-grpc_opt paths=source_relative \
--grpc-gateway_out ./pkg/grpc --grpc-gateway_opt paths=source_relative \
./proto/*.proto
```

## Server実装

ビルドした*.protoを使用してServerにメソッドを実装します。

下記のようにSever Processを実装するために`main.go`を作成します。
```
sample-book-lending % tree ./cmd
./cmd
├── client
└── server
    └── main.go

```

server/main.go
```
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc/reflection"
	hellopb "sample-book-lending/pkg/grpc"
	"log"
	"os"
	"os/signal"

	// (一部抜粋)
	"google.golang.org/grpc"
	"net"

	// for proxy
	"google.golang.org/grpc/credentials/insecure"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"net/http"
)

type myServer struct {
	hellopb.UnimplementedLendingBooksServiceServer
}

// account.protoの`service`に定義したメソッドの実装
// 本を借りるためのメソッド
func (s *myServer) SendBorrow(ctx context.Context, req *hellopb.BorrowRequest) (*hellopb.BorrrowResponse, error) {
	// リクエストからnameフィールドを取り出して
	// "Hello, [名前]!"というレスポンスを返す
	return &hellopb.BorrrowResponse{
		Account: &hellopb.Account{Name: req.Account.Name},
		Book: &hellopb.Book{Title: req.Book.Title},
	}, nil
}

// account.protoの`service`に定義したメソッドの実装
// アカウントの貸与状態を知るためのRestful API
func (s *myServer) AccountInfo(ctx context.Context, req *hellopb.Account) (*hellopb.Account, error) {
	// リクエストからnameフィールドを取り出して
	// "Hello, [名前]!"というレスポンスを返す
	return &hellopb.Account{
		Name: req.GetName(),
	}, nil
}

// 自作サービス構造体のコンストラクタを定義
func NewMyServer() *myServer {
	return &myServer{}
}

func main() {
	// 1. 8080番portのLisnterを作成
	port := 8080
	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		panic(err)
	}

	// 2. gRPCサーバーを作成
	s := grpc.NewServer()

	// 3. gRPCサーバーにGreetingServiceを登録
	// 第二引数はinterfaceであるGreetingServiceServerのため、これのメソッドリストを実装した構造体がはいる。
	hellopb.RegisterLendingBooksServiceServer(s, NewMyServer())

	// x_numberはproxy serverのインスタンス作成と起動です。
	// x_1. for proxy
	// Create a client connection to the gRPC server we just started
	// This is where the gRPC-Gateway proxies the requests
	conn, err := grpc.NewClient(
		"0.0.0.0:8080",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)

	gwmux := runtime.NewServeMux()

	err = hellopb.RegisterLendingBooksServiceHandler(context.Background(), gwmux, conn)
	if err != nil {
		log.Fatalln("Failed to register gateway:", err)
	}

	gwServer := &http.Server{
		Addr:    ":8090",
		Handler: gwmux,
	}

	// 4. サーバーリフレクションの設定
	reflection.Register(s)

	// 5. 作成したgRPCサーバーを、8080番ポートで稼働させる
	go func() {
		log.Printf("start gRPC server port: %v", port)
		s.Serve(listener)
	}()

	// x_2. for proxy
	log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
	log.Fatalln(gwServer.ListenAndServe())

	// 6.Ctrl+Cが入力されたらGraceful shutdownされるようにする
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("stopping gRPC server...")
	s.GracefulStop()
}

```

### サーバーを起動

goを実行します。
```
cd cmd/server && go run main.go
```

```
2024/09/27 16:37:41 Serving gRPC-Gateway on http://0.0.0.0:8090
2024/09/27 16:37:41 start gRPC server port: 8080
```

## Client

Restful APIとgRPC APIを実装しています。
リクエストを送信してみます。

call gRPC API
```
grpcurl -plaintext -d '{"account":{ "name": "Tanaka"},"book":{ "title": "赤毛のあん"}}' localhost:8080 myapp.LendingBooksService.SendBorrow
```

```
{
  "account": {
    "name": "Tanaka"
  },
  "book": {
    "title": "赤毛のあん"
  }
}
```

call Restful API
```
curl -X POST -k http://localhost:8090/v1/accountinfo/books -d '{"name": " Proxy Anpanman"}'
```

```
{"name":" Proxy Anpanman"}
```

## Doc生成

### Install protoc-gen-doc
protoc-gen-docをインストールしてください。
```
go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@latest
```

### ドキュメント生成

protofileからドキュメントを生成します。コマンドでprofofileから仕様書を生成できるので、ドキュメント管理が楽です。

```
protoc -I ./proto --doc_out=html,index.html:./doc proto/*.proto
```

生成された[Protocol Documentation](https://kynea0b.github.io/demo-protocol-buffers/sample-book-lending/doc/index.html)です。



今回の全コードは[こちら](https://github.com/Kynea0b/demo-protocol-buffers/tree/main/sample-book-lending)。