🦔

gRPCのハローワールドをやってみた(Go)

2022/09/10に公開

最初に

gRPCのハローワールドをやってみたので備忘録です。
内容はクライアントからPinを渡すと、サーバーからPon!が返ってくるだけの実装です。
ちなみに当方Goは初心者です。

% go run client/main.go Pin 
Pon!

https://github.com/t-shiratori/hello-grpc-go

環境

チップ OS Go
Apple M1 Pro macOS Monterey go1.18 darwin/arm64

gRPCの特徴

  • Googleが2015年に開発したオープンソース
  • いろんな環境で実行できるRPC(Remote Procedure Call)のフレームワーク
  • protocol buffersをIDL(インターフェイス定義言語)として使用
    • Googleが2008年に開発したオープンソースのスキーマ言語
  • 11言語に対応
  • HTTP2で通信
  • gRPCのgはGoogleのgではない。リリースごとに意味が違う。

https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md

開発のおおまかな流れ

  • protoファイルを作成する(プロトコルバッファの定義)
  • protoファイルをコンパイルしてサーバー、クライアントの雛形のコードを生成する
  • 雛形のコードを利用してサーバーとクライアントを実装する

実際にやったプロジェクトのファイル構成。

📦hello-grpc-go
 ┣ 📂client
 ┃ ┗ 📜main.go
 ┣ 📂go-protocol-buffer
 ┃ ┣ 📜pin-pon.pb.go
 ┃ ┗ 📜pin-pon_grpc.pb.go
 ┣ 📂proto
 ┃ ┗ 📜pin-pon.proto
 ┣ 📂server
 ┃ ┗ 📜main.go
 ┣ 📜go.mod
 ┗ 📜go.sum

環境構築

公式のガイド
https://grpc.io/docs/languages/go/quickstart/

Goをインストール

インストール方法はいろいろあるので好きなやり方で入れておきます。
一応公式のリンクだけ載せておきます。

https://go.dev/doc/install

Protocol Buffersのコンパイラをインストール

Homebrewでインストール

brew install protobuf

バージョンを確認

% protoc --version
libprotoc 3.19.4

プロトコルバッファをコンパイルするためのGo用のプラグインをインストールする

  • protoc-gen-go
    Goのプロトコルバッファパッケージを生成するprotocプラグイン。

  • protoc-gen-go-grpc
    protoファイルで定義したサービスと連携させるためのGoのコードを生成するプラグイン。

protocはプロトコルバッファからコードを生成するためのコンパイラのことのようです。
参考: protocプラグインの書き方 > protoc

$ 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

バージョンを確認

% protoc-gen-go --version
protoc-gen-go v1.28.0
% protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.2.0

実装

プロジェクト用のディレクトリを作成

mkdir hello-grpc-go
cd hello-grpc-go

Protocol Buffers の定義

proroファイル用のディレクトリを作成。

mkdir proto

下記の用に定義を作成。

proto/pin-pon.proto
syntax = "proto3";

// プロトファイルの名前空間を定義
package PinPon;

// 変換後のgoファイルを出力するディレクトリかつパッケージ名
option go_package = "./go-protocol-buffer";

message pinPonRequest {
    string words = 1; // 数字はフィールド番号(タグ)
};

message pinPonResponse {
    string words = 1; // 数字はフィールド番号(タグ)
};

service PinPonService {
    rpc send (pinPonRequest) returns (pinPonResponse);
}

以下の箇所はプロトコルバッファの仕様で、シリアライズされたデータから値を特定するために、プロパティに対してフィールド番号(タグ)を指定しています。

string words = 1; // 数字はフィールド番号(タグ)

サービスがRPCのメソッドのまとまりです。サービス内に定義したメソッドをサーバー側で実装し、クライアント側から呼び出して実行します。

Protocol Buffers をコンパイルしてGoの定義ファイルを出力する

protoc -I. --go_out=. --go-grpc_out=. proto/*.proto

-I.はprotoファイルをimport文で検索するための起点になるディレクトリを指定するオプション。
--go-grpc_out=.はプロトファイルからgrpcのサーバーとクライアントの雛形を作成するためのオプション。=.はファイルを出力したいパスを指定します。

実行すると以下のようになります。

📦hello-grpc-go
 ┣ 📂go-protocol-buffer
 ┃ ┣ 📜pin-pon.pb.go
 ┃ ┗ 📜pin-pon_grpc.pb.go
 ┣ 📂proto
 ┃ ┗ 📜pin-pon.proto

サーバー側の実装

  • 通信の確立
  • プロトコルバッファから生成した構造体を使ってサーバーの処理を作成
  • gRPCのサーバーを作成
  • サービスにgRPCサーバーと実装したサーバーの処理を登録
  • サーバーの起動
package main

import (
	"context"
	"fmt"
	go_protocol_buffer "hello-grpc-go/go-protocol-buffer"
	"log"
	"net"

	"google.golang.org/grpc"
)

type server struct {
	go_protocol_buffer.UnimplementedPinPonServiceServer
}

func (s *server) Send(ctx context.Context, req *go_protocol_buffer.PinPonRequest) (*go_protocol_buffer.PinPonResponse, error) {

	resWords := ""

	if req.Words == "Pin" {
		resWords = "Pon!"
	} else {
		resWords = "Please need words 'Pin'!"
	}

	res := &go_protocol_buffer.PinPonResponse{
		Words: resWords,
	}

	return res, nil
}

func main() {
	listener, err := net.Listen("tcp", "localhost:50051")

	if err != nil {
		log.Fatalf("Failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer()

	go_protocol_buffer.RegisterPinPonServiceServer(grpcServer, &server{})

	fmt.Println("server is runnig...")

	if err := grpcServer.Serve(listener); err != nil {
		log.Fatalf("Failed to serve: %v", err)
	}

}

クライアント側の実装

  • grpcのコネクションを確立
  • プロトコルバッファから生成したコードを使ってサービスのクライアントを作成
  • クライアントを使ってデータを送信
  • レスポンスを受け取る
package main

import (
	"context"
	"fmt"
	go_protocol_buffer "hello-grpc-go/go-protocol-buffer"
	"log"
	"os"

	"google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())

	if err != nil {
		log.Fatalf("Failed to connect: %v", err)
	}

	defer conn.Close()

	client := go_protocol_buffer.NewPinPonServiceClient(conn)

	callPinPon(client)
}

func callPinPon(client go_protocol_buffer.PinPonServiceClient) {

	reqWords := ""

	if len(os.Args) >= 2 {
		reqWords = os.Args[1]
	}

	res, err := client.Send(context.Background(), &go_protocol_buffer.PinPonRequest{Words: reqWords})

	if err != nil {
		log.Fatalln(err)
	}

	fmt.Println(res.GetWords())
}

実行

サーバー側を起動

go run server/main.go
server is runnig...

クライアント側を実行

コマンドライン引数でPinを渡すと、Pon!が返ってきます。

% go run client/main.go Pin 
Pon!

コマンドライン引数でPin以外を渡すと、Please need words 'Pin'!と返ってきます。

go run client/main.go hoge
Please need words 'Pin'!

最後に

ミニマムの構成で作ってみてgRPCについてどんなものかを体系的に理解することを趣旨としてやってみました。自分なりに用語の整理をしながらやっていってある程度はどういうものかが理解できた気がします。

今回gRPCのキャッチアップでこちらのUdemyの講座をやってみましたが、体系的に情報がまとまっていて環境構築から実装までの手順もわかりやすく個人的にはいい内容でした。
https://www.udemy.com/course/go-grpc-x/

Discussion