今回は Go で gRPC から REST API Server をつくる方法の備忘録です。
Go のバージョンは 1.11 です。
$ go version
go version go1.11 darwin/amd64
gRPC
gRPC (gRPC Remote Procedure Calls) は Google が開発した OSS の RPC (remote procedure call) フレームワーク。
grpc.io では主な特徴として以下が紹介されている。
- Idiomatic client libraries in 10 languages
- Highly efficient on wire and with a simple service definition framework
- Bi-directional streaming with http/2 based transport
- Pluggable auth, tracing, load balancing and health checking
http/2 による双方向ストリーミングが可能で認証や LB などプラガブルに機能拡張できる機構を持っている。また, サービス定義はシンプルで10言語のクライアントライブラリをサポートしているため特定の言語やプラットフォームに依存しない。
データシリアライゼーションには同じく Google の Protocol Buffers が使われている。
Protocol Buffers
Protocol Buffers はシリアライズフォーマットで, その構造を定義するためのインタフェース記述言語 (Interface Description Language) でもある。 .proto ファイルに Message や Services を定義する。言語仕様は Language Guide (proto3) を確認する。
releases から 環境に合わせた zip を選択しインストールする。 grpc-gateway を使うために ProtocolBuffers 3.0.0-beta-3 以降のバージョンが必要。
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-osx-x86_64.zip
$ mkdir protoc-3.6.1
$ unzip protoc-3.6.1-osx-x86_64.zip -d protoc-3.6.1/
Archive: protoc-3.6.1-osx-x86_64.zip
creating: protoc-3.6.1/include/
creating: protoc-3.6.1/include/google/
creating: protoc-3.6.1/include/google/protobuf/
inflating: protoc-3.6.1/include/google/protobuf/wrappers.proto
inflating: protoc-3.6.1/include/google/protobuf/field_mask.proto
inflating: protoc-3.6.1/include/google/protobuf/api.proto
inflating: protoc-3.6.1/include/google/protobuf/struct.proto
inflating: protoc-3.6.1/include/google/protobuf/descriptor.proto
inflating: protoc-3.6.1/include/google/protobuf/timestamp.proto
creating: protoc-3.6.1/include/google/protobuf/compiler/
inflating: protoc-3.6.1/include/google/protobuf/compiler/plugin.proto
inflating: protoc-3.6.1/include/google/protobuf/empty.proto
inflating: protoc-3.6.1/include/google/protobuf/any.proto
inflating: protoc-3.6.1/include/google/protobuf/source_context.proto
inflating: protoc-3.6.1/include/google/protobuf/type.proto
inflating: protoc-3.6.1/include/google/protobuf/duration.proto
creating: protoc-3.6.1/bin/
inflating: protoc-3.6.1/bin/protoc
inflating: protoc-3.6.1/readme.txt
$ ln -s ~/tmp/protoc-3.6.1/bin/protoc /usr/local/bin/protoc
$ which protoc
/usr/local/bin/protoc
$ protoc --version
libprotoc 3.6.1
protoc コマンドは protocol buffer compiler で .proto ファイルを parse し各言語のコードを生成する機能を持っている。
gRPC-Go
gRPC の Go 実装である gRPC-Go をインストールする。
go get -u google.golang.org/grpc
google.golang.org/grpc は後述する EchoServiceServer の実装で使う。
grpc-gateway
grpc-gateway は gRPC サービス定義 (.proto) を基に gRPC を RESTful JSON API に翻訳する reverse-proxy server を生成するツール。
(出展: github.com/grpc-ecosystem/grpc-gateway)
まず, grpc-gateway のインストール。
$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
$ go get -u github.com/golang/protobuf/protoc-gen-go
今回は examples にある Echo Service を例とする。 (echo_service.proto)
syntax = "proto3";
option go_package = "examplepb";
// Echo Service
//
// Echo Service API consists of a single service which returns
// a message.
package grpc.gateway.examples.examplepb;
import "google/api/annotations.proto";
// Embedded represents a message embedded in SimpleMessage.
message Embedded {
oneof mark {
int64 progress = 1;
string note = 2;
}
}
// SimpleMessage represents a simple message sent to the Echo service.
message SimpleMessage {
// Id represents the message identifier.
string id = 1;
int64 num = 2;
oneof code {
int64 line_num = 3;
string lang = 4;
}
Embedded status = 5;
oneof ext {
int64 en = 6;
Embedded no = 7;
}
}
// Echo service responds to incoming echo requests.
service EchoService {
// Echo method receives a simple message and returns it.
//
// The message posted as the id parameter will also be
// returned.
rpc Echo(SimpleMessage) returns (SimpleMessage) {
option (google.api.http) = {
post: "/v1/example/echo/{id}"
additional_bindings {
get: "/v1/example/echo/{id}/{num}"
}
additional_bindings {
get: "/v1/example/echo/{id}/{num}/{lang}"
}
additional_bindings {
get: "/v1/example/echo1/{id}/{line_num}/{status.note}"
}
additional_bindings {
get: "/v1/example/echo2/{no.note}"
}
};
}
// EchoBody method receives a simple message and returns it.
rpc EchoBody(SimpleMessage) returns (SimpleMessage) {
option (google.api.http) = {
post: "/v1/example/echo_body"
body: "*"
};
}
// EchoDelete method receives a simple message and returns it.
rpc EchoDelete(SimpleMessage) returns (SimpleMessage) {
option (google.api.http) = {
delete: "/v1/example/echo_delete"
};
}
}
protoc コマンドを使い .proto から gRPC stub (echo_service.pb.go) を生成する。
$ protoc -I/usr/local/bin -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=plugins=grpc:. \
proto/echo_service.proto
EchoServiceServer の実装。 (echo.go)
package main
import (
"context"
"github.com/golang/glog"
examples "../proto"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// Implements of EchoServiceServer
type echoServer struct{}
func newEchoServer() examples.EchoServiceServer {
return new(echoServer)
}
func (s *echoServer) Echo(ctx context.Context, msg *examples.SimpleMessage) (*examples.SimpleMessage, error) {
glog.Info(msg)
return msg, nil
}
func (s *echoServer) EchoBody(ctx context.Context, msg *examples.SimpleMessage) (*examples.SimpleMessage, error) {
glog.Info(msg)
grpc.SendHeader(ctx, metadata.New(map[string]string{
"foo": "foo1",
"bar": "bar1",
}))
grpc.SetTrailer(ctx, metadata.New(map[string]string{
"foo": "foo2",
"bar": "bar2",
}))
return msg, nil
}
func (s *echoServer) EchoDelete(ctx context.Context, msg *examples.SimpleMessage) (*examples.SimpleMessage, error) {
glog.Info(msg)
return msg, nil
}
gRPC Service を起動する Server の実装。 (server.go)
package main
import (
"context"
"flag"
"net"
examples "../proto"
"github.com/golang/glog"
"google.golang.org/grpc"
)
// Run starts the example gRPC service.
// "network" and "address" are passed to net.Listen.
func run(ctx context.Context, network, address string) error {
l, err := net.Listen(network, address)
if err != nil {
return err
}
defer func() {
if err := l.Close(); err != nil {
glog.Errorf("Failed to close %s %s: %v", network, address, err)
}
}()
s := grpc.NewServer()
examples.RegisterEchoServiceServer(s, newEchoServer())
go func() {
defer s.GracefulStop()
<-ctx.Done()
}()
return s.Serve(l)
}
func main() {
flag.Parse()
defer glog.Flush()
ctx := context.Background()
if err := run(ctx, "tcp", ":9090"); err != nil {
glog.Fatal(err)
}
}
gRPC Server を起動する。
$ go run server.go echo.go
gRPC Client
gRPC Server の疎通確認のため gRPC Client から接続してみる。 grpc_cli はインストールのためにビルドが必要で時間がかかるため Evans を選択した。
$ go get -u github.com/ktr0731/evans
.proto の配置を以下のようにした。 (google/api/annotations.proto, google/api/http.proto)
$ tree -P *.proto proto/ google/
proto/
└── echo_service.proto
google/
└── api
├── annotations.proto
└── http.proto
1 directory, 3 files
gRPC Server を起動している状態で Evans を REPL Mode で起動する。
$ evans --port 9090 proto/echo_service.proto
______
| ____|
| |__ __ __ __ _ _ __ ___
| __| \ \ / / / _. | | '_ \ / __|
| |____ \ V / | (_| | | | | | \__ \
|______| \_/ \__,_| |_| |_| |___/
more expressive universal gRPC client
127.0.0.1:9090> show package
+---------------------------------+
| PACKAGE |
+---------------------------------+
| grpc.gateway.examples.examplepb |
| google.api |
+---------------------------------+
127.0.0.1:9090> package grpc.gateway.examples.examplepb
grpc.gateway.examples.examplepb.EchoService@127.0.0.1:9090> show service
+-------------+------------+---------------+---------------+
| SERVICE | RPC | REQUESTTYPE | RESPONSETYPE |
+-------------+------------+---------------+---------------+
| EchoService | Echo | SimpleMessage | SimpleMessage |
| | EchoBody | SimpleMessage | SimpleMessage |
| | EchoDelete | SimpleMessage | SimpleMessage |
+-------------+------------+---------------+---------------+
grpc.gateway.examples.examplepb@127.0.0.1:9090> service EchoService
grpc.gateway.examples.examplepb.EchoService@127.0.0.1:9090> show message
+---------------+
| MESSAGE |
+---------------+
| Embedded |
| SimpleMessage |
+---------------+
grpc.gateway.examples.examplepb.EchoService@127.0.0.1:9090> call Echo
? code line_num
line_num (TYPE_INT64) => 1
? ext en
en (TYPE_INT64) => 1
id (TYPE_STRING) => 1
num (TYPE_INT64) => 1
? mark note
status::note (TYPE_STRING) => 1
{
"id": "1",
"num": 1,
"lineNum": 1,
"lang": "",
"status": {
"progress": 0,
"note": "1"
},
"en": 1,
"no": null
}
gRPC Server のデバッグに便利。
RESTful JSON API Server
次に protoc コマンドを使い reverse-proxy (echo_service.pb.gw.go) を生成する。
$ protoc -I/usr/local/bin -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:. \
proto/echo_service.proto
echo_service.pb.gw.go が gRPC を RESTful JSON API に翻訳する機能を提供する。
gRPC Service への接続と HTTP Server を起動する部分の実装が以下。 (api_server.go)
package main
import (
"flag"
"net/http"
"github.com/golang/glog"
"golang.org/x/net/context"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
gw "../proto"
)
var (
echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService")
)
func run() error {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
err := gw.RegisterEchoServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts)
if err != nil {
return err
}
return http.ListenAndServe(":8080", mux)
}
func main() {
flag.Parse()
defer glog.Flush()
if err := run(); err != nil {
glog.Fatal(err)
}
}
gRPC Server が起動している状態で, API Server を起動する。
$ go run api_server.go
API Server の確認のため HTTP Request を投げてみる。
$ curl -vvv -X POST "http://127.0.0.1:8080/v1/example/echo/hoge" -H "accept: application/json"
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /v1/example/echo/hoge HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> accept: application/json
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Grpc-Metadata-Content-Type: application/grpc
< Date: Sun, 23 Sep 2018 01:41:09 GMT
< Content-Length: 13
<
* Connection #0 to host 127.0.0.1 left intact
{"id":"hoge"}
request path で使用していない message のフィールドは query parameter (e.g., `?en=1&status.note=abc`) として与えることでメソッド内で拾える。
$ curl -vvv -X POST "http://127.0.0.1:8080/v1/example/echo/hoge?en=1&status.note=done" -H "accept: application/json"
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /v1/example/echo/hoge?en=1&status.note=done HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> accept: application/json
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Grpc-Metadata-Content-Type: application/grpc
< Date: Sun, 23 Sep 2018 01:43:10 GMT
< Content-Length: 47
<
* Connection #0 to host 127.0.0.1 left intact
{"id":"hoge","status":{"note":"done"},"en":"1"}
gRPC から REST API Server をつくる部分は以上で完了。
Swagger
Swagger は REST API を記述するための仕様。 grpc-gateway の機能を使って Swagger による REST API 仕様を生成することができる。
$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
protoc コマンドで echo_service.swagger.json を生成する。
$ protoc -I/usr/local/bin -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--swagger_out=logtostderr=true:. \
proto/echo_service.proto
Swagger Editor を DL し起動する。
$ wget https://github.com/swagger-api/swagger-editor/archive/v3.6.10.zip
$ unzip v3.6.10.zip
$ npm install http-server
$ mv swagger-editor-3.6.10 swagger-editor
$ ./node_modules/.bin/http-server swagger-editor
起動後, echo_service.swagger.json を読み込む。 Swagger Editor は編集しながらプレビューを確認でき便利。
[1] Swagger UI
[2] grpcc
[3] Proto3 Language Guide(和訳)
[4] grpc-gatewayの開発に学ぶ、ソフトウェアの設計手法