grpcを用いたgreetingサーバ/クライアント をGo言語で作成する

@asya_aoi1049 on Wed Jun 16 2021
18.1 min

目次

gRPCとは

gRPCの公式サイトの文章を参照すると以下のような説明があります。

gRPC is a modern open source high performance RPC framework that can run in any environment.
It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication.
It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.

引用元: gRPC

上記から

  • gRPCはRPCのフレームワークであり
  • サービスとデータセンター(サーバ)間を効率的に接続することができ
  • モバイルアプリケーションやブラウザ及びバックエンドサービスといった様々なサービスと接続できる

ことが読み取れます。

そもそもRPCとは何かと言うと、remote procedure call (リモートプロシージャコール)といって、インターネット等を介して遠隔地にある(別のコンピュータ上にある)ソフトウェアを操作するためのプロトコルです。

先頭のgはGoogleが開発したことに由来するgなので、gRPCの概要をまとめると Googleが開発したRPC(リモートプロシージャコール)フレームワーク となります。

greeting サーバ/クライアントの概要

本稿では、gRPCを体験するためにgreetingサーバ/クライアントを作成します。

機能

greetingサーバ/クライアントの機能は以下の通りです。

greetingサーバ

  • localhost:50051 でクライアントからのAPI呼び出しを受け付ける
  • クライアントからリクエストを受け取り、リクエストに含まれる内容をレスポンスとしてクライアントに返す

greetingクライアント

  • localhost:50051 に対してAPIを呼び出す
  • クライアントはリクエストを送り、その後サーバからのレスポンスを受け取る
  • 受け取った内容を標準出力に表示する

※echoサーバ/クライアントのようなものですね。
以降では、これらの機能を満たすようgRPCでAPIを作成し、そのAPIをやり取りするためのサーバ/クライアントを作成していきます。

Go modでプログラミングする環境を整える

Go111から実装されたmodを利用しサーバ/クライアントを実装するため、実装に必要なパッケージをインストールします。
以下のコマンドを実行し、Go moduleを利用します。

# go modを利用できるよう設定
$ export GO111MODULE=on
$ cd work
# Go moduleファイルの作成
$ go mod init greet
$ ls go.mod
go.mod
# grpcモジュールをインストール
$ go get -u google.golang.org/grpc
# protoc コマンドで Goモジュールに変換するためのプラグインをインストール
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ ls $GOPATH/bin/protoc-gen-go
/home/toshiki/bin/protoc-gen-go

以降では、gRPCを利用するために、どのようにgRPCでAPIを定義するか、また定義ファイルをどのようにGo モジュール化するかを説明します。

gRPCでAPIを作成する

本節ではgRPCを用いてgreetingサーバ/クライアント間で利用するAPIを定義します。

どのようにAPIを定義するか

gRPCでは、protocol buffers(プロトコルバッファ)というInterface Definition Language (IDL)を用いてAPIを定義します。

プロトコルバッファはXMLのようなものと思っていただいても良いかと思いますが、XMLよりも簡潔且つ少ないコード量で記述できます。

プロトコルバッファの記述例:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}

参考: protocol buffers

プロトコルバッファを用いてgreetingサーバ/クライアント間でやり取りするプロトコルバッファメッセージ(API仕様)を定義し、サーバ/クライアントの両者が同じ定義を参照します。
そのため、API仕様のズレを防いだりAPI不整合による不具合を検知できる等のメリットがあります。
プロトコルバッファについてより詳しい記述方法や仕様を知りたい方は、 protocol buffersprotocol buffers developer guide を参照して下さい。

まとめるとgRPCはプロトコルバッファでメッセージ(API)を定義する となります。

greeting プロトコルバッファメッセージ(API)を定義する

greetingプロトコルバッファメッセージでは、ただ1つのフィールド:Name を持つように定義してみます。
つまり、クライアントから

{
  "Name": "XXXXX"
}

というようなリクエストを受け取り、サーバは

{
  "Message": "XXXXX"
}

このNameフィールドの内容をレスポンスとして返すというAPI仕様を実装します。

作業ディレクトリに移動し、以下の流れで .protoファイルを作成します。

# 自身の作業用ディレクトリに移動
$ cd work
# protocファイルの作成
$ mkdir helloworld
$ cd helloworld
# エディタ(ここではemacs)でファイルを編集する
$ emacs helloworld.proto
syntax = "proto3";

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;
}
$ ls
helloworld.proto

その後、protocというコマンドを用いてGo言語のモジュールにコンパイルします。
linux(x86-64)の場合、protocコマンドはバイナリパッケージで提供されているためこれを利用します。

protoc binary downloads (または protoc binary releases )からzip圧縮されたファイル(protoc-3.7.1-linux-x86_64.zip)をダウンロードし解凍及びPATHの通っているディレクトリ等に配置します。

# ダウンロード後
$ cd Downloads
$ unzip protoc-3.7.1-linux-x86_64.zip
$ ls
bin includes
# PATHの通っている任意のディレクトリに配置
$ cp bin/protoc $HOME/bin/.
$ which protoc
/home/toshiki/bin/protoc

protocコマンドを適用し、Go言語で利用可能なモジュールを生成します。

$ cd work
$ ls -d helloworld
helloworld/
$ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld
$ ls helloworld
helloworld.pb.go  helloworld.proto

helloworld.pb.goをサーバ/クライアントの両方から利用することで共通のAPI定義を参照できます。

greetサーバ/クライアントを実装する

APIの定義が完了したので、次はサーバ/クライアントの実装を行います。

サーバの実装

サーバは以下のように実装できます。

$ cd work
$ mkdir greeter_server
$ cd greeter_server
# エディタ(ここではemacs)でmain.goを新規作成及び編集する
$ emacs main.go
// Package main implements a server for Greeter service.
package main

import (
        "context"
        "log"
        "net"

        "google.golang.org/grpc"
        // greetモジュールで管理するため <モジュール名>/helloworldとなる
        pb "greet/helloworld"
)

const (
        port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        log.Printf("Received: %v", in.Name)
        return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
        lis, err := net.Listen("tcp", port)
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }
        s := grpc.NewServer()
        pb.RegisterGreeterServer(s, &server{})
        if err := s.Serve(lis); err != nil {
                log.Fatalf("failed to serve: %v", err)
        }
}

クライアントの実装

クライアントは以下のように実装できます。

$ cd work
$ mkdir greeter_client
$ cd greeter_client
# エディタ(ここではemacs)でmain.goを新規作成及び編集する
$ emacs main.go
// Package main implements a client for Greeter service.
package main

import (
        "context"
        "log"
        "os"
        "time"

        "google.golang.org/grpc"
        // greetモジュールで管理するため <モジュール名>/helloworldとなる
        pb "greet/helloworld"
)

const (
        address     = "localhost:50051"
        defaultName = "world" // Nameフィールドは "world" を入れている
)

func main() {
        // Set up a connection to the server.
        conn, err := grpc.Dial(address, grpc.WithInsecure())
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        // Contact the server and print out its response.
        name := defaultName
        if len(os.Args) > 1 {
                name = os.Args[1]
        }
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.Message)
}

テスト

コンソールを2つ開き、1つではサーバプログラムの実行、もう1つではクライアントプログラムの実行を行います。

$ go run greeter_server/main.go

$ go run greeter_client/main.go
2019/05/03 18:26:18 Greeting: Hello world

まとめ

gRPCを利用することで、サーバ/クライアント間で共通のAPIを利用することができ、またAPIのコードが分離しないためリクエスト/レスポンスフィールドがズレることなく実装できます。
Go言語で.protoファイルを含めたモジュール化(バージョン管理)を行う際には、モジュール名から始まるパスを指定する必要があります。

参考

日別に記事を見る