GolangとgRPC #464
Goのキャッチアップを進めていて、GoではgRPCが使用されることが多い、という情報を得たので少し寄り道して整理してみます。
gRPCはオープンソースのAPI アーキテクチャおよびシステムで、REST等とよく比較されます。
RPCは Remote Procedure Call の略で、その名の通りリモート(別サーバー)で動作している特定のプロシージャ(機能)を呼び出して使用できます。gRPC は、このRPCにいくつかの最適化を加えたGoogleが開発したシステムです。
gRPC はデータ送信にプロトコルバッファと HTTP 2 を使用しており、双方向ストリーミング(やクライアントorサーバーからの片方向ストリーミング)をサポートしています。
gRPCの特徴
gRPPCの大きな特徴は、複数のリクエストを一括で送信できる点です。対照的に、REST APIは1リクエスト1レスポンスで、操作の続行には応答を待つ必要があります。
また、gPRCで呼び出し可能な操作はサービスで定義できます(サービス自体はプロトコルバッファで定義)。要求動詞がGETやPOSTなどに限られるRESTとはここも対照的です。
gRPCでは、レスポンスやサービスの定義にプロトコルバッファを使用します。これはバイナリ化して送信されるため、JSONのように人間が読める形式ではない代わりに、JSONより高速です。
GolangでgPRCを使ってみる
やってみるに当たって、こちらの動画を大変参考にさせていただきました。
最終的なフォルダ構成は以下です。
s_grpc --- api ------- helloworld -- helloworld_grpc.pb.go # pbから自動生成するファイル
| | ` helloworld.pb.go # pbから自動生成するファイル
| |- go.mod
| ` server.go # サーバーを定義するGoファイル
|
|- client --- src - helloworld - proto -- helloworld_gprc_web_pb.d.ts # pbから自動生成するファイル
| | |- helloworld_gprc_web_pb.js # pbから自動生成するファイル
| | |- helloworld_pb.d.ts # pbから自動生成するファイル
| | ` helloworld_pb.js # pbから自動生成するファイル
| ` その他Svelte関連のフォルダ
|
|- proto --- helloworld.proto
|
|- proxy --- Dockerfile
| ` envoy.yaml
|
` generate_code.sh
サンプル用のクライアント(フロント画面)を準備
動画の真似をして、簡単に画面を準備できるsvelteを使います。以下のコマンドを打つだけで、必要なファイルがclientディレクトリに揃います。
$ npx degit sveltejs/template client
$ cd client
$ yarn
$ yarn dev
これでブラウザからhttp://localhost:8080でアクセスできるはずです(ポートはターミナルに出ます)。
プロトコルバッファからコードを生成
公式サンプルから.protoの内容をコピーし、go_packageのパスを今回用に修正します。これがプロトコルバッファです。
[helloworld.proto]
syntax = "proto3";
option go_package = "api/helloworld;helloworld"; // 後ほどgoファイルを出力する先。ここだけ公式サンプルから変更する。
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
作成したプロトコルバッファからGoやJavaScriptファイルを生成するprotocコマンドを使えるようにするため、protobufをインストールします。
$ brew install protobuf
protocでgoファイルを生成するためのプラグインをインストールします。これは公式サンプル通りです。インストールしたらパスも通します。
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
$ export PATH="$PATH:$(go env GOPATH)/bin"
今回はJavaScriptファイルも生成するので、同じくprotocを使うためのプラグインをインストールします。これはgrpc-webのREADMEに沿っています。
$ npm install -g protoc-gen-js
今回grpc-web (ブラウザ側) のコードをJavaScriptで自動生成しますが、そのためのプラグインも必要です。公式のリリースページから最新のものをダウンロードし、以下のように移動させたうえで権限を変更します。
Downloads $ sudo mv protoc-gen-grpc-web-1.5.0-darwin-aarch64 /usr/local/bin/protoc-gen-grpc-web
Downloads $ chmod +x /usr/local/bin/protoc-gen-grpc-web
ルートにシェルファイルを作り、プロトコルバッファからコードを生成するコマンドを生成します。
[generate_code.sh]
#! /bin/sh
protoc proto/helloworld.proto \
--js_out=import_style=commonjs:client/src/helloworld \
--grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:client/src/helloworld \
--go-grpc_out=.
--go_out=.
これでgenerate_code.shを実行すれば、プロトコルバッファからGoとJavaScriptのコードが生成されるはずです。
Goでサーバーを定義
続いてGoでサーバーを立てます。gRPC-Webを使う場合、envoyというプロキシ経由でリクエストを受け付ける必要があるらしく、
このenvoyはブラウザからのリクエストを受け付け、指定したポートに転送します。そしてGoのサーバーでもそのポートをリッスンしておくことで、envoy経由でリクエストを受信する仕組みです。
[server.go]
package main
import (
"context"
"log"
"net"
pb "s_grpc/helloworld"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
type HelloworldHandler struct {
pb.UnimplementedGreeterServer
}
func (h HelloworldHandler) SayHello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello" + request.Name}, nil
}
func main() {
port := ":9090"
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatal("failed to listen %v", err)
}
server := grpc.NewServer()
pb.RegisterGreeterServer(server, &HelloworldHandler{})
reflection.Register(server)
log.Printf("start gPRC server")
server.Serve(lis)
}
s_grpc/helloworldでインポートしているのは、プロトコルバッファで自動生成したgRPC用のhelloworldパッケージです。インポートする自作パッケージのパスは、モジュール名を起点として、go.modファイルからの相対パスを記載すればOKです。
func main を軸に解説していきます。
port := ":9090"
envoyではデフォルトでポート9090に転送するので、サーバーは9090をリッスンします。
lis, err := net.Listen("tcp", port)
ネットワークリスナーを作成します。net.Listenは指定されたネットワーク(ここではtcp)とアドレス(ここでは9090)で待ち受けるリスナーを返します。
server := grpc.NewServer()
新しいgRPCサーバーを作成します。grpc.NewServerはデフォルトのオプションで新しいgRPCサーバーを返します。これはポインタ型です。
pb.RegisterGreeterServer(server, &HelloworldHandler{})
GreeterサービスをgRPCサーバーに登録します。RegisterGreeterServerは、引数にServiceRegistrarインターフェースとGreeterServerインターフェースを期待しています。上部で定義しているHelloworldHandlerは、GreeterServerインターフェースを満たすように定義したものです。
reflection.Register(server)
gRPCサーバーにリフレクションサービスを登録します。リフレクションは、クライアントが動的にサーバーのメタデータを取得するための補助的な機能を提供します。開発やデバッグ時に役立ちます。
server.Serve(lis)
サーバーを指定されたリスナー(lis)で開始します。Serveメソッドは、gRPCサーバーがリクエストを受け入れて処理するようになります。このメソッドはブロッキングメソッドであり、サーバーが停止するまで戻りません。
SayHello関数
今回フロント画面からリモートで呼び出す関数です。Helloという文字列に、リクエストに含まれるNameフィールドの文字を加えて返します。
プロキシ(envoy)を定義
先ほど触れたように、gRPC-Webを使う場合、envoyをプロキシに立てる必要があります。これはDockerで構築できます。
まず、gRPC-Webの公式サンプルからenvoy.yamlのコードをコピーし、proxyディレクトリに格納します。公式サンプルに内容から、2箇所だけ修正します(コード中にメモしています)。
[envoy.yaml]
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8085 } # 元々はport_value: 8080だったものを修正
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: greeter_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: greeter_service
connect_timeout: 0.25s
type: logical_dns
# HTTP/2 support
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
lb_policy: round_robin
# win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal # 元々は0.0.0.0だったものを修正
port_value: 9090
Dockerファイルは以下のように設定します。Docker hubに公式イメージがあるので活用します。
[Dockerfile]
FROM envoyproxy/envoy:v1.30.0
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
EXPOSE 8085
ここで、受付ポートは8085にしています。デフォルトでは8080だったのですが、それだとSvelteで使っているポートと被ってしまったためです。この8085向けに、フロント画面からリクエストを飛ばします。
リクエスト用ボタンをフロントに設置
今回は簡単に、svelteのルート画面にJavaScriptで直接定義します。
プロトコルバッファから自動生成したファイルをインポートして使用しています。もし自動生成ファイルの中で「ライブラリが見つからない」などのエラーが出ていたら、以下のようにyarn addしてみてください。
s_grpc/client $ yarn add grpc-web
s_grpc/client $ yarn add google-protobuf
リクエスト用のコードは自動生成されているので、比較的簡単に使用できます。GreeterPromiseClientで8085へリクエストするよう定義しています。
[App.svelte]
<script>
import {GreeterPromiseClient} from './helloworld/proto/helloworld_grpc_web_pb'
import {HelloRequest} from './helloworld/proto/helloworld_pb'
export let name;
let resp = ''
const client = new GreeterPromiseClient('http://localhost:8085', null, null)
const handleClick = () => {
console.log('click!!')
const request = new HelloRequest()
request.setName('Worldddddd')
client.sayHello(request).then((reply) => {
console.log(reply.getMessage())
resp = reply.getMessage()
})
}
</script>
<main>
<h1>Hello {name}!</h1>
<p>{resp}</p>
<p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
<button on:click={handleClick}>テストボタン</button>
</main>
テストボタンをクリックすると、GoサーバーにgRPCでリクエストを叩き、レスポンス内容が<p>{resp}<p>に表示されます。
これで全ての準備が整いました。
それぞれ起動してリクエストしてみる
ターミナルを3つ起動してそれぞれを起動します。
[goサーバーを起動]
s_grpc/api $ go run serve.go
[フロント画面を起動]
s_grpc/client $ yarn dev
[プロキシを起動]
s_grpc/proxy $ docker run -it -p 8085:8085 envoy
この画面が立ち上がり、
テストボタンをクリックするとレスポンス内容が表示されます。
シンプルな内容ですが、GoでgPRCを使ってみることができました!
ここまでお読みいただきありがとうございました。