LoginSignup
14
8

More than 3 years have passed since last update.

Golangで始めるgRPC

Posted at

概要

gRPCはあらゆる環境で実行できるモダンで高性能なオープンソースRPC(Remoto Protocol Call)フレームワークです。Protocol Buffersを使ってデータをシリアライズし、高速な通信を実現できるという利点がある。様々な言語やプラットフォームに対応しており、http/2を活かした双方向ストリーミングなどが可能である。Protocol Buffersを使用してシンプルにサービス(通信するデータや関数)を定義でき、APIの仕様を明文化できる。

※この章はgRPCを参考にしています。

git: k-washi/example-golang-gRPC

スタートプロジェクト

VSCodeの設定

  • vscode-proto3 & Clang-Format extensions をインストール

もし、自動でファイルをフォーマットしたいならば、以下も設定する。

  • Clang-Formatをインストール
brew install clang-format #MacOSX

#other OS  
#http://www.codepool.biz/vscode-format-c-code-windows-linux.html

Protocol buffersの設定

Protocol buffersのインストール

# MacOSX
brew install protobuf #MacOSX
# Linux
#https://github.com/google/protobuf/releases

#example
# Make sure you grab the latest version
curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip
# Unzip
unzip protoc-3.5.1-linux-x86_64.zip -d protoc3
# Move protoc to /usr/local/bin/
sudo mv protoc3/bin/* /usr/local/bin/
# Move protoc3/include to /usr/local/include/
sudo mv protoc3/include/* /usr/local/include/
# Optional: change owner
sudo chown [user] /usr/local/bin/protoc
sudo chown -R [user] /usr/local/include/google

Golangの設定

パッケージののインストール

go get -u google.golang.org/grpc
go get -d -u github.com/golang/protobuf/protoc-gen-go

実行の確認

まず、サービスを定義する。

greet.proto
syntax = "proto3";

package greet;
option go_package="greetpb";

service GreetService{}

この定義したファイルに対して、以下のコマンドでgolang用のprotocol buffers(greet.pb.go)に変換する。この変換されたライブラリファイル(greet.pb.go)内の関数を、golangによるクライアントとサーバーで使用することで、gRPCによる通信を実現できる。

protoc greet/greetpb/greet.proto  --go_out=plugins=grpc:.

クライアントは、以下のように接続の確立を行い、クライアントAPI(NewGreetServiceClient)で通信している。

client.go
package main

import (
    "fmt"
    "log"
  //protocol buffersライブラリー
    "github.com/k-washi/example-golang-gRPC/greet/greetpb"
    "google.golang.org/grpc"
)

func main() {
    fmt.Println("Hello. I'm a client")
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Could not connect: %v", err)
    }
    defer conn.Close()
    c := greetpb.NewGreetServiceClient(conn)
    fmt.Printf("Created client %f", c)
}

サーバーは、構造体serverを定義し、サービスをサーバーに登録する。そして、サーバーを立ち上げる。

server.go
package main

import (
    "fmt"
    "log"
    "net"
    "github.com/k-washi/example-golang-gRPC/greet/greetpb"
    "google.golang.org/grpc"
)

type server struct{}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }

    s := grpc.NewServer()
    greetpb.RegisterGreetServiceServer(s, &server{})

    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

以下のコマンドで、サーバーを立ち上げ、クライアントを実行することで、実行の確認を行う。


#server setup
go run greet/greet_server/server.go 

#client setup
go run greet/greet_client/client.go

通信手法

gRPCは、以下のような通信手法があり、データの特性に応じて適宜選択する必要がある。

  • Unary - もっとも一般的なgRPCのAPIで、クライアントからのメッセージ1つに対して、サーバーは1回のメッセージを返す
  • Server streaming - HTTP/2の恩恵を受けたAPIで、クライアントからのメッセージ1つに対して、サーバーは複数回のメッセージを返す
  • Client streaming - HTTP/2の恩恵を受けたAPIで、クライアントからの複数のメッセージに対して、サーバーは1回のメッセージを返す

サービスの定義

  1. 送信するメッセージの型を定義する
  2. Unary, Server streaming, Client streamingのリクエスト・レスポンスにおけるメッセージの型を定義する
  3. サービスを rpc サービス名(stream クライアントの型) returns (stream レスポンスの型) {}; として定義する。 複数回送るサービスのメッセージにはstreamをつける。
  4. 以下のコマンドでprotocol buffersに変換.
protoc streaming/greetpb/greet.proto  --go_out=plugins=grpc:.
syntax = "proto3";

package greet;
option go_package="greetpb";

//リクエストメッセージの型を定義
message Greeting {
  string first_name = 1;
  string last_name = 2;
}

// Unary リクエスト
message GreetRequest {
  Greeting greeting = 1;
}

// Unary レスポンス
message GreetResponse {
  string result = 1;
}

// Server streaming リクエスト
message GreetManyTimesRequest {
  Greeting greeting = 1;
}

// Server streaming レスポンス
message GreetManyTimesResponse {
  string result = 1;
}

// Client streaming リクエスト
message LongGreetRequest {
  Greeting greeting = 1;
}

// Client streaming レスポンス
message LongGreetResponse {
  string result = 1;
}

// サービスの定義
service GreetService{
  //Unary
  rpc Greet(GreetRequest) returns (GreetResponse) {};

  //Server Streaming
  rpc GreetManyTimes(GreetManyTimesRequest) returns (stream GreetManyTimesResponse) {};

  //Client Streaming
  rpc LongGreet(stream LongGreetRequest) returns (LongGreetResponse) {};
}

クライアント

Golangにより、Unary, Server streaming, Client streamingのクライアントを実装する。基本的には、サービス定義に合わせた型でデータを作成、リクエストを送信、そして、レスポンスを待つ形式である。これらの処理には、protocol buffersライブラリに記載されたパッケージを使用する。サーバーからの複数回のメッセージを待つ場合、繰り返し構文(for)を用いて受信することで、適用できる。複数回サーバーにメッセージを送信する場合も、繰り返し構文を用いているが、基本的には、メッセージごとに送信しているだけである。gRPCに関するエラーは、"google.golang.org/grpc/status"ライブラリを用いて抽出・定義できる。

client.go

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "time"

    "google.golang.org/grpc/codes"

    "github.com/k-washi/example-golang-gRPC/streaming/greetpb"
    "google.golang.org/grpc"
    "google.golang.org/grpc/status"
)

func main() {
    fmt.Println("Hello. I'm a client")
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Could not connect: %v", err)
    }
    defer conn.Close()
    c := greetpb.NewGreetServiceClient(conn)

    doUnary(c)
    doServerStreaming(c)
    doClientStreaming(c)

}

func doUnary(c greetpb.GreetServiceClient) {
    fmt.Println("Starting to do a Unary RPC ...")
    req := &greetpb.GreetRequest{
        Greeting: &greetpb.Greeting{
            FirstName: "tanaka",
            LastName:  "tarou",
        },
    }

    res, err := c.Greet(context.Background(), req)

    if err != nil {
        respErr, ok := status.FromError(err)
        if ok {
            fmt.Println(respErr.Message())
            fmt.Println(respErr.Code())
            if respErr.Code() == codes.InvalidArgument {
                fmt.Println("We probably sent a empty string")

            } else if respErr.Code() == codes.DeadlineExceeded {
                fmt.Println("Timeout was hit! Deadline was exceeded")
            }
        } else {
            log.Printf("error while calling greet rpc: %v", err)
        }
    }
    fmt.Printf("Response from greet server %v", res)
}

func doServerStreaming(c greetpb.GreetServiceClient) {
    fmt.Println("Starting to do a Server Streaming RPC ...")
    req := &greetpb.GreetManyTimesRequest{
        Greeting: &greetpb.Greeting{
            FirstName: "tanaka",
            LastName:  "tarou",
        },
    }
    resStream, err := c.GreetManyTimes(context.Background(), req)
    if err != nil {
        log.Fatalf("error while calling GreetManyTImes RPC: %v", err)
    }
    for {
        msg, err := resStream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("error while reading stream: %v", err)
        }
        log.Printf("Response from GreetManyTImes: %v", msg.GetResult())
    }

}

func doClientStreaming(c greetpb.GreetServiceClient) {
    fmt.Println("Starting to do a Client Streaming RPC")

    requests := []*greetpb.LongGreetRequest{
        &greetpb.LongGreetRequest{
            Greeting: &greetpb.Greeting{
                FirstName: "one",
            },
        },
        &greetpb.LongGreetRequest{
            Greeting: &greetpb.Greeting{
                FirstName: "two",
            },
        },
        &greetpb.LongGreetRequest{
            Greeting: &greetpb.Greeting{
                FirstName: "three",
            },
        },
        &greetpb.LongGreetRequest{
            Greeting: &greetpb.Greeting{
                FirstName: "four",
            },
        },
    }

    stream, err := c.LongGreet(context.Background())
    if err != nil {
        log.Fatalf("error while calling LongGreet %v", err)
    }
    for _, req := range requests {
        fmt.Printf("Sending req: %v\n", req)
        stream.Send(req)
        time.Sleep(1000 * time.Millisecond)
    }

    res, err := stream.CloseAndRecv()
    if err != nil {
        log.Fatalf("error while receiving response from LongGreet: %v", err)
    }
    fmt.Printf("LongGreet Response: %v", res)

}

サーバー

server構造体に、protocol buffersに定義されたGreetServiceServerと同じ形式で関数を埋め込む。
メッセージの送受信もprotocol buffersに定義されたものを用いている。また、リクエストの取得にも、メッセージに対するGet関数がprotocol bufferに用意されている。

server.go
package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net"
    "strconv"
    "time"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    "github.com/k-washi/example-golang-gRPC/streaming/greetpb"

    "google.golang.org/grpc"
)

//server greet.pb.go GreetServiceClient
type server struct{}

func (*server) Greet(ctx context.Context, req *greetpb.GreetRequest) (*greetpb.GreetResponse, error) {
    fmt.Printf("Greet func was invoked with %v", req)
    firstName := req.GetGreeting().GetFirstName()
    if firstName == "" {
        return nil, status.Errorf(
            codes.InvalidArgument,
            "Recived a empty string",
        )
    }

    if ctx.Err() == context.Canceled {
        return nil, status.Error(codes.Canceled, "the client canceld the request")
    }

    result := "Hello " + firstName
    //client config deadline
    /*
        res := &greetpb.GreetWithDeadlineResponse{
            Result: result,
        }
    */
    res := &greetpb.GreetResponse{
        Result: result,
    }
    return res, nil
}

func (*server) GreetManyTimes(req *greetpb.GreetManyTimesRequest, stream greetpb.GreetService_GreetManyTimesServer) error {
    fmt.Printf("GreetManyTimes func was invoked with %v", req)
    firstName := req.GetGreeting().GetFirstName()
    for i := 0; i < 10; i++ {
        result := "Hello" + firstName + " number " + strconv.Itoa(i)
        res := &greetpb.GreetManyTimesResponse{
            Result: result,
        }
        stream.Send(res)
        time.Sleep(1000 * time.Millisecond)
    }
    return nil
}

func (*server) LongGreet(stream greetpb.GreetService_LongGreetServer) error {
    fmt.Println("LongGreet function was invoked with a streaming request")
    result := ""
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            //we have finished reading the client stream
            return stream.SendAndClose(&greetpb.LongGreetResponse{
                Result: result,
            })
        }
        if err != nil {
            log.Fatalf("Error while reading client stream: %v", err)
        }
        firstName := req.GetGreeting().GetFirstName()
        result += "Hello " + firstName + "! "

    }
}

func main() {
    fmt.Println("Hello World!")

    lis, err := net.Listen("tcp", "0.0.0.0:50051")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }

    s := grpc.NewServer()
    greetpb.RegisterGreetServiceServer(s, &server{})

    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

参考文献

14
8
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
14
8