2
4

More than 3 years have passed since last update.

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

Posted at

はじめに

少し前にgRPCの入門記事を書きました。
前回記事:Go言語でgRPCの門を叩いたお話
その時はgRPC通信でserver側のメソッドを利用しましたが、今回はprotoファイルに少しオプションを追加することでAPIも用意できるようにしてくれる gRPC-gateway の門を叩こうと思います。

準備

必要なprotocol buffersのライブラリをインストールするところから始めます。

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/golang/protobuf/protoc-gen-go

また下記のGoパッケージが必要になるので自動でインストールしてくれるようgo.modファイルを生成しておきます。

  • github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
  • github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2

go.mod生成

go mod init

実装

今回はそのまま実装を見てもらったほうが早いと思うのでさっそく実装に入っていきます。
使うメソッドは前回同様、名前と年齢を渡して挨拶コメントが返ってくるもので試していきます。
github: https://github.com/jgvkmea/grpc-gateway-entry

ディレクトリ構造

.
├── README.md
├── go.mod
├── go.sum
├── grpc
│   └── main.go
├── pb
│   ├── sample.pb.go
│   ├── sample.pb.gw.go
│   └── sample.proto
└── rest
    └── main.go

protoファイル

gRPCでインターフェースを定義するときと同様にリクエストとレスポンスのmessage型とserviceを定義します。
まずはgRPC-gatewayを使うために必要なprotoファイルをimportします。

import "google/api/annotations.proto";

gRPC-gateway を使う場合は下のフォーマットでAPIのURLを定義します。

option (google.api.http) = {
  HTTPメソッド: "APIのパス"  {フィールド名} でパスにパラメータ指定も可能)
  (リクエストボディがある場合)
  body: "フィールド指定"  (リクエストのフィールドすべての場合は * を指定)
};

protoファイル全体はこちらになります。

pb/sample.proto
syntax = "proto3";

package sample;

option go_package = "./pb";

import "google/api/annotations.proto";

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

message HelloPersonMessageResponse {
  string hello_message = 1;
}

service Hello {
  rpc HelloPerson (HelloPersonMessageRequest) returns (HelloPersonMessageResponse) {
    option (google.api.http) = {
      get: "/v1/greet"
    };
  }
}

Goファイル生成

まずはgRPCの時と同様、.pb.goファイルを生成するコマンドを叩きます。
gRPCの時のコマンドに加え、annotations.protoをimportするためのパスを追加してます。(2行目)

protoc -I. \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  ./pb/sample.proto

ちなみに-Iオプションはimportするprotoファイルのパスを指定しているので、-Iでパス指定した分protoファイル内のimportは省略できます。

例: annotations.proto
// ファイルパス
$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api/annotations.proto

// -I で下記を指定する場合
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api

// protoファイル内のimport文は下記でOK
import "annotations.proto";

次は.gw.pb.goファイルを生成します。
先程のprotocコマンドの3行目を書き換えて以下のコマンドで生成します。

protoc -I. \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=. \
  ./pb/sample.proto

gRPC server 実装

まずはgRPC側のserverから実装していきます。
といっても前と同じです。

grpc/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "net"

    "github.com/jgvkmea/grpc-gateway-entry/pb"

    "google.golang.org/grpc"
)

// HelloServer serverの構造体
type HelloServer struct{}

var grpcPort = ":8080"

// 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
}

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

REST API server 実装

次にAPIのサーバーを実装していきます。
基本はgithubのサンプルから取ってきて必要箇所だけ変更してます。
サンプル: https://github.com/grpc-ecosystem/grpc-gateway#usage の5番です。

変更箇所

  • grpcServerEndpoint のポート番号をgRPC serverでリッスンしているポートに変更
  • RegisterYourServiceHandlerFromEndpointを自分の実装したものに合わせて名前を変える。

実際に変更したあとのコードはこちらです。

rest/main.go
package main

import (
    "context"
    "flag"
    "net/http"

    "github.com/golang/glog"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"

    gw "github.com/jgvkmea/grpc-gateway-entry/pb"
)

var (
    // command-line options:
    // gRPC server endpoint
    grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:8080", "gRPC server endpoint")
    restPort           = ":8081"
)

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // Register gRPC server endpoint
    // Note: Make sure the gRPC server is running properly and accessible
    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}
    err := gw.RegisterHelloHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
    if err != nil {
        return err
    }

    // Start HTTP server (and proxy calls to gRPC server endpoint)
    return http.ListenAndServe(restPort, mux)
}

func main() {
    flag.Parse()
    defer glog.Flush()

    if err := run(); err != nil {
        glog.Fatal(err)
    }
}

実行

それではここまで実装したものを実行してみます。
まずは gRPC server から起動してみます。

$ go run grpc/main.go

続いて別のセッションから REST API server を起動します。

$ go run rest/main.go

この状態でさらに別セッションからcurlでAPIを叩いてみます。

$ curl 'localhost:8081/v1/greet?name=Qiita&age=9'
{"helloMessage":"Qiitaさん(0歳)、こんにちは。"}

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

おわりに

前回に引き続き、今回はgRPC-gatewayを使って前回作ったサーバーをAPIとして公開できるようにしてみました。
APIの設定等かなり平易で初心者でもすぐに使えそうな印象でした。
ぜひ皆さんも門を叩いてみてください。

2
4
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
4