見出し画像

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レスポンスで、操作の続行には応答を待つ必要があります。

gRPC では、クライアントは 1 つまたは複数の API リクエストをサーバーに送信でき、その結果、サーバーから 1 つまたは複数の応答が返される場合があります。データ接続には、単項 (1 対 1)、サーバーストリーミング (1 対多数)、クライアントストリーミング (多数対 1)、または双方向ストリーミング (多数対多数) があります。このメカニズムはクライアント/レスポンス通信モデルであり、gRPC が HTTP 2 に基づいているため可能です。

gRPC と REST の違いはなんですか?

また、gPRCで呼び出し可能な操作はサービスで定義できます(サービス自体はプロトコルバッファで定義)。要求動詞がGETやPOSTなどに限られるRESTとはここも対照的です。

gRPC API では、呼び出し可能なサーバー操作はサービス (関数またはプロシージャとも呼ばれます) によって定義されます。gRPC クライアントは、アプリケーション内で内部的に関数を呼び出すのと同じように、これらの関数を呼び出します。これはサービス指向設計と呼ばれます。

gRPC と REST の違いはなんですか?

gRPCでは、レスポンスやサービスの定義にプロトコルバッファを使用します。これはバイナリ化して送信されるため、JSONのように人間が読める形式ではない代わりに、JSONより高速です。

gRPC はデフォルトでプロトコルバッファ (Protobuf) 形式を使用しますが、ネイティブの JSON サポートも提供しています。サーバーは、proto 仕様ファイル内のプロトコルバッファインターフェイス記述言語 (IDL) を使用してデータ構造を定義します。次に、gRPC はその構造をバイナリ形式にシリアル化し、指定された任意のプログラミング言語に逆シリアル化します。このメカニズムにより、送信中に圧縮されない JSON を使用するよりも高速になります。プロトコルバッファは、JSON で使用される REST API とは異なり、人間が読める形式ではありません。

gRPC と REST の違いはなんですか?


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

protocgoファイルを生成するためのプラグインをインストールします。これは公式サンプル通りです。インストールしたらパスも通します。

$ 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というプロキシ経由でリクエストを受け付ける必要があるらしく、

gRPC-web clients connect to gRPC services via a special proxy; by default, gRPC-web uses Envoy.

https://github.com/grpc/grpc-web

この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

リクエスト用のコードは自動生成されているので、比較的簡単に使用できます。GreeterPromiseClient8085へリクエストするよう定義しています。

[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を使ってみることができました!
ここまでお読みいただきありがとうございました。


参考




この記事が気に入ったらサポートをしてみませんか?