1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

golangでgRPC作ってみた

Posted at

Mac(M1)使ってます。

作りたいサービス

名前送ったら(Cli)、なんか(SV)返ってくるやつ。
具体的には以下の4つのメソッドを作成する。

  1. 名前を送ると、”Hello 名前”って返ってくる。
  2. 名前を送ると、”Hello 名前!”, "Hello 名前!!"みたいに「!」が増えて返ってくる。
  3. 名前を複数回送ると、まとめて"Hello 名前1、名前2、、、"って返ってくる。
  4. 名前を何個も送ると、送った分"Hello 名前"って返ってくる。

gRPCとは(超簡単に)

これよかった

ディレクトリ構成

current dir
    └──hello
          ├── client
          ├── proto
          └── server

まずはProtocol Bufferの記述

hello/proto/hello.proto
// バージョン
syntax = "proto3";

// Goのパッケージ名として使われる。
// 別Protoファイルで同じmessageが使用されていた場合に衝突を回避できる。
package hello;

// Goのインポートパス
option go_package = "github.com/hirohokke/grpc-go/hello/proto";

// リクエストを宣言(Goのstructのようなもの)
message HelloRequest {
    // 型 フィールド名 = タグ;
    string first_name = 1;
}

// レスポンスを宣言(Goのstructのようなもの)
message HelloResponse {
    string result = 1;
}

// サービスを宣言
// ここに宣言されたRPCをクライアントから呼び出す
service HelloService {
    // リクエストを渡して、レスポンスを返す
    rpc Hello (HelloRequest) returns (HelloResponse);
    // レスポンスにstreamを付けることでSVは複数レスポンスを返せる
    rpc HelloAmp (HelloRequest) returns (stream HelloResponse);
    // リクエストにstreamを付けることでクライアントは複数リクエストを渡せる
    rpc HelloManyTimes (stream HelloRequest) returns (HelloResponse);
    // リクエスト、レスポンスにstreamを付けることで双方向でストリーミングできる
    rpc HelloEveryone (stream HelloRequest) returns (stream HelloResponse);
}

詳しいことは、公式
とか、これ日本語

protobufについて簡単に超ざっくり説明

  • データを構造化してシリアライズするための、Googleによって作られた機構。
  • バイナリでシリアライズされるので、サイズが小さくPayloadが小さくて通信が早くなる。(よく使われるJSONやXMLに比べて←プレーンテキスト)
  • 型宣言するので、型の安全性が保証される。
  • いろんな言語に対応
    etc.

protobufのコンパイル

まず、protobufのインストール

$ brew install protobuf

Go用のprotobufランタイムをインストール

$ go get -u google.golang.org/grpc
$ go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc

Go用にコンパイル

$ protoc -Ihello/proto \
         --go_opt=module=github.com/hirohokke/grpc-go \
         --go_out=. \
         --go-grpc_opt=module=github.com/hirohokke/grpc-go\
         --go-grpc_out=. hello/proto/*.proto

こうすると、hello/proto配下にgoコードが生成される。
-Iは依存関係を解決するみたい。今回はないけど。

Goファイル作成

準備ができたので、Goファイルの作成
※エラーハンドリングしてたりしてなかったり等ありますが、気にしないでください。適当です。

Server

hello/server/main.go
package main

import (
	pb "github.com/hirohokke/grpc-go/hello/proto"
	"google.golang.org/grpc"
	"log"
	"net"
)

// gRPCサーバーで受け付けるアドレス
var addr string = "0.0.0.0:50051"

// gRPCサーバーの構造体を宣言
type Server struct {
	pb.HelloServiceServer
}

func main() {
	// 指定したアドレスでTCPプロトコルで通信受付インスタンス生成
	lis, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatalf("Failed to listen on: %v\n", err)
	}

	log.Printf("Listen on %s\n", addr)

	// grpcサーバーを生成し、Helloサービスを設定
	s := grpc.NewServer()
	pb.RegisterHelloServiceServer(s, &Server{})

	// サーバー起動
	if err = s.Serve(lis); err != nil {
		log.Fatalf("Failed to serve: %v\n", err)
	}
}

次に、protobufに記述したRPCサービスを実装

hello/client/hello.go
package main

import (
	"context"
	"fmt"
	pb "github.com/hirohokke/grpc-go/hello/proto" // protobufのインポート
	"io"
	"log"
	"strings"
)

// 1. 名前を送ると、”Hello FirstName”を返す
func (s *Server) Hello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	log.Printf("Helloが呼び出されました: %v\n", in)
	// ここでは、文字列を生成し、レスポンスを返すだけ。
	return &pb.HelloResponse{
		Result: fmt.Sprintf("Hello %s", in.FirstName),
	}, nil
}

// 2. !が増幅して返ってくる
func (s *Server) HelloAmp(in *pb.HelloRequest, stream pb.HelloService_HelloAmpServer) error {
	log.Printf("HelloAmpが呼び出されました: %v\n", in)

	for i := 0; i < 5; i++ {
		// レスポンス作成
		res := fmt.Sprintf("Hello %s%s", in.FirstName, strings.Repeat("!", i))

		// クライアントにレスポンスを送信
		err := stream.Send(&pb.HelloResponse{Result: res})
		if err != nil {
			log.Fatalf("レスポンス送信中にエラーが発生: %v\n", err)
		}

	}

	return nil
}

// 3. 複数回送られてくるリクエストを結合して返す
func (s *Server) HelloManyTimes(stream pb.HelloService_HelloManyTimesServer) error {
	log.Println("HelloManyTimesが呼び出されました。")

	res := "Hello"

	// 無限ループでリクエストを受け取り続ける
	for {
		// リクエストを取得
		req, err := stream.Recv()

		// リクエスト終端の場合、レスポンスを送信
		if err == io.EOF {
			return stream.SendAndClose(&pb.HelloResponse{
				Result: res,
			})
		}

		// 終端以外のエラーの場合
		if err != nil {
			log.Fatalf("リクエスト読み取りでエラー発生: %v\n", err)
		}

		res = res + " " + req.FirstName
	}
}

// 4. リクエストごとに返信する
func (s *Server) HelloEveryone(stream pb.HelloService_HelloEveryoneServer) error {
	log.Println("HelloEveryoneが呼び出されました")

	for {
		// streamからリクエストを受け取り
		req, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			log.Fatalf("リクエスト読み取り中にエラー発生: %v\n", err)
		}

		res := fmt.Sprintf("Hello %s", req.FirstName)
		// streamにレスポンスを送信
		err = stream.Send(&pb.HelloResponse{Result: res})
		if err != nil {
			log.Fatalf("レスポンス送信中にエラー発生: %v\n", err)
		}
	}
}

次にクライアント

Client

hello/client/main.go
package main

import (
	pb "github.com/hirohokke/grpc-go/hello/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
)

// 作成したgRPCサーバーのアドレス
var addr string = "localhost:50051"

func main() {
	// 作ったサーバーとのコネクタを作成
	// ssl認証なし
	conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("接続失敗: %v\n", err)
	}
	defer conn.Close()

	// gRPCクライアントの生成
	c := pb.NewHelloServiceClient(conn)

	doHello(c)
	doHelloAmp(c)
	doHelloManyTimes(c)
	doHelloEveryone(c)
}

呼び出し部分

hello/client/hello.go
package main

import (
	"context"
	pb "github.com/hirohokke/grpc-go/hello/proto"
	"io"
	"log"
	"time"
)

// 1. Hello呼び出し
func doHello(c pb.HelloServiceClient) {
	log.Println("---doHello---")

	// リクエストを作成
	req := &pb.HelloRequest{FirstName: "Hokke"}

	// Helloサービス呼び出し
	res, err := c.Hello(context.Background(), req)
	if err != nil {
		log.Fatalf("Helloの呼び出しでエラー: %v\n", err)
	}
	log.Printf("%s\n", res.Result)
}

// 2. HelloAmp呼び出し
func doHelloAmp(c pb.HelloServiceClient) {
	log.Println("---doHelloAmp---")

	// リクエスト作成
	req := &pb.HelloRequest{FirstName: "Hokke"}

	// gRPCとのstream生成
	stream, err := c.HelloAmp(context.Background(), req)
	if err != nil {
		log.Fatalf("HelloAmp呼び出しでエラー: %v\n", err)
	}

	// EOFが送られるまでレスポンスを取得し続けるため、無限ループ
	for {
		// レスポンス取得
		res, err := stream.Recv()

		// レスポンスが終わった場合
		if err == io.EOF {
			break
		}

		if err != nil {
			log.Fatalf("レスポンス取得でエラー発生: %v\n", err)
		}

		log.Printf("Response is %s\n", res.Result)
	}
}

// 3. HelloManyTimes呼び出し
func doHelloManyTimes(c pb.HelloServiceClient) {
	log.Println("---doHelloManyTimes---")

	// stream生成
	stream, err := c.HelloManyTimes(context.Background())
	if err != nil {
		log.Fatalf("HelloManyTimes呼び出しでエラー")
	}

	// リクエスト生成
	reqs := []*pb.HelloRequest{
		{FirstName: "Hokke"},
		{FirstName: "Hiro"},
		{FirstName: "Hoge"},
	}

	// リクエスト数分送信
	for _, req := range reqs {
		log.Printf("Sending Request: %v\n", req)
		stream.Send(req)
	}

	res, err := stream.CloseAndRecv()
	if err != nil {
		log.Fatalf("レスポンス取得でエラー: %v\n", err)
	}

	log.Println(res.Result)
}

// 4. HelloEveryone呼び出し
func doHelloEveryone(c pb.HelloServiceClient) {
	log.Println("---doHelloEveryone---")

	stream, err := c.HelloEveryone(context.Background())
	if err != nil {
		log.Fatalf("呼び出しでエラー")
	}

	reqs := []*pb.HelloRequest{
		{FirstName: "Hokke"},
		{FirstName: "Hiro"},
		{FirstName: "Hoge"},
	}

	// ゴルーチンを使うので、待機チャネルを使用
	waitc := make(chan struct{})

	go func() {
		for _, req := range reqs {
			log.Printf("Sending request: %v\n", req)
			stream.Send(req)
			// わかりやすいように、1s待つ
			time.Sleep(1 * time.Second)
		}
		stream.CloseSend()
	}()

	go func() {
		for {
			res, err := stream.Recv()
			if err == io.EOF {
				break
			}
			if err != nil {
				log.Fatalf("レスポンス取得中にエラー: %v\n", err)
			}
			log.Printf("Response is %s\n", res.Result)
		}
		close(waitc)
	}()

	<-waitc
}

実行

サーバー、クライアントそれぞれモジュール作成

$ go build -o bin/server ./hello/server
$ go build -o bin/client ./hello/client

あとは実行するだけ

terminal1
$ ./bin/server
2023/04/26 22:04:55 Listen on 0.0.0.0:50051
terminal2
$ ./bin/client
2023/04/26 22:05:03 ---doHello---
2023/04/26 22:05:03 Hello Hokke
2023/04/26 22:05:03 ---doHelloAmp---
2023/04/26 22:05:03 Response is Hello Hokke
2023/04/26 22:05:03 Response is Hello Hokke!
2023/04/26 22:05:03 Response is Hello Hokke!!
2023/04/26 22:05:03 Response is Hello Hokke!!!
2023/04/26 22:05:03 Response is Hello Hokke!!!!
2023/04/26 22:05:03 ---doHelloManyTimes---
2023/04/26 22:05:03 Sending Request: first_name:"Hokke"
2023/04/26 22:05:03 Sending Request: first_name:"Hiro"
2023/04/26 22:05:03 Sending Request: first_name:"Hoge"
2023/04/26 22:05:03 Hello Hokke Hiro Hoge
2023/04/26 22:05:03 ---doHelloEveryone---
2023/04/26 22:05:03 Sending request: first_name:"Hokke"
2023/04/26 22:05:03 Response is Hello Hokke
2023/04/26 22:05:04 Sending request: first_name:"Hiro"
2023/04/26 22:05:04 Response is Hello Hiro
2023/04/26 22:05:05 Sending request: first_name:"Hoge"
2023/04/26 22:05:05 Response is Hello Hoge
terminal1クライアント実行後
$ ./bin/server 
2023/04/26 22:04:55 Listen on 0.0.0.0:50051
2023/04/26 22:05:03 Helloが呼び出されました: first_name:"Hokke"
2023/04/26 22:05:03 HelloAmpが呼び出されました: first_name:"Hokke"
2023/04/26 22:05:03 HelloManyTimesが呼び出されました。
2023/04/26 22:05:03 HelloEveryoneが呼び出されました

クライアントで一気にサービスを呼び出してるけど、別々で呼び出した方が処理わかりやすい。

おまけ

エラーについて

サーバーからクライアントにエラーも返せる

server.go
// 処理
if err != nil {
    return status.Errorf(
        codes.Internal, // 例)内部エラー
        fmt.Sprinf("内部エラー発生"),
    )
}
client.go
// 例
err := callGRPC()
if err != nil {
    e, ok := status.FromError(err)
    if ok {
        if e.Code() == codes.Internal {
            // エラーハンドリング
        } else {
            log.Fatalf("Unexpected gRPC error: %v\n", err)
        }
    } else {
        log.Fatalf("A non gRPC error: %v\n", err)
    }
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?