とほほのgRPC入門

目次

gRPCとは

インストール

CentOS 8 + Python の場合の例を示します。インストールにはそこそこの時間がかかります。

# CentOS 8 + Python
# yum -y install gcc-c++ python3 python3-devel
# pip3 install grpcio==1.39.0 grpcio-tools==1.39.0

プロトファイルを作成する

プロトコルバッファを定義するプロトファイル(hello.proto)を作成します。

hello.proto
syntax = "proto3";

package hello;

service HelloWorld {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

スタブを作成する

protoc を用いてスタブを作成します。

# python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./hello.proto

下記のファイルが作成されます。

hello.proto
hello_pb2.py
hello_pb2_grpc.py

サーバプログラムを作成する

.proto から生成されたサービサーを用いて、サーバプログラム(server.py)を作成します。

server.py
from concurrent import futures
import grpc
import hello_pb2
import hello_pb2_grpc

class HelloWorld(hello_pb2_grpc.HelloWorldServicer):
    def SayHello(self, request, context):
        print("RECV: %s" % request.name)
        message = "Hello, %s!" % request.name
        print("SEND: %s" % message)
        return hello_pb2.HelloReply(message=message)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    hello_pb2_grpc.add_HelloWorldServicer_to_server(HelloWorld(), server)
    server.add_insecure_port('[::]:8000')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

クライアントプログラムを作成する

.proto から生成されたスタブを用いて、クライアントプログラム(client.py)を作成します。

client.py
import grpc
import hello_pb2
import hello_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:8000') as channel:
        stub = hello_pb2_grpc.HelloWorldStub(channel)
        response = stub.SayHello(hello_pb2.HelloRequest(name='Yamada'))
    print("RECV: %s" % response.message)

if __name__ == '__main__':
    run()

サーバとクライアントを実行する

まずはサーバを起動します。

# python3 ./server.py

次にクライアントを起動します。

# python3 ./client.py
Hello, Yamada!

サーバから Hello, Yamada! というメッセージが返却されれば成功です。

Go言語の場合

Go言語の場合のサンプルを示します。Go 1.15, protoc 3.17, gRPC 1.39

必要なパッケージをインストールしておきます。

# dnf -y install golang wget unzip
# go version
go version go1.15.13 linux/amd64

protoc コマンドをインストールします。

# mkdir ~/protoc
# cd ~/protoc
# wget https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protoc-3.17.3-linux-x86_64.zip
# unzip ./protoc-3.17.3-linux-x86_64.zip
# ~/protoc/bin/protoc --version
libprotoc 3.17.3

Go 1.12 からは GOPATH モードではなく、モジュールモードが推奨されるそうなので、モジュールモードにして、必要なモジュールをダウンロードします。

# export GO111MODULE=on
# export PATH="$PATH:$(go env GOPATH)/bin"
# go get google.golang.org/protobuf/cmd/protoc-gen-go@v1.27
# go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1

サンプルとなる hello モジュールを作成します。

# mkdir ~/hello
# cd ~/hello
# go mod init example.com/my-name/hello

プロトファイルを作成してコンパイルします。

# mkdir ./proto
# vi ./proto/hello.proto
proto/hello.proto
syntax = "proto3";

option go_package = "example.com/my-name/hello";

package hello;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
# ~/protoc/bin/protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \
  --go-grpc_opt=paths=source_relative ./proto/hello.proto

サーバプログラムを作成します。

# mkdir ./server
# vi ./server/main.go
server/main.go
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "example.com/my-name/hello/proto"
)

type server struct {
    pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("RECV: %v", in.GetName())
    message := "Hello, " + in.GetName() + "!"
    log.Printf("SEND: %v", message)
    return &pb.HelloReply{Message: message}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    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)
    }
}

クライアントプログラムを作成します。

# mkdir ./client
# vi ./client/main.go
client/main.go
package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    pb "example.com/my-name/hello/proto"
)

func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Yamada"})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("RECV: %s", r.GetMessage())
}

ターミナルを二つ起動し、サーバプログラムと、クライアントプログラムを実行します。いくつかのパッケージが自動インストールされます。Go 1.16 からは自動インストールはされなくなるので、いくつか事前にインストールしておく必要があるかもしれません。

# go run ./server/main.go
go: finding module for package google.golang.org/grpc
go: finding module for package google.golang.org/grpc/status
go: finding module for package google.golang.org/protobuf/reflect/protoreflect
go: finding module for package google.golang.org/protobuf/runtime/protoimpl
go: finding module for package google.golang.org/grpc/codes
go: downloading google.golang.org/grpc v1.39.0
go: found google.golang.org/grpc in google.golang.org/grpc v1.39.0
go: found google.golang.org/grpc/codes in google.golang.org/grpc v1.39.0
go: found google.golang.org/grpc/status in google.golang.org/grpc v1.39.0
go: found google.golang.org/protobuf/reflect/protoreflect in google.golang.org/protobuf v1.27.1
go: found google.golang.org/protobuf/runtime/protoimpl in google.golang.org/protobuf v1.27.1
go: downloading github.com/golang/protobuf v1.5.0
go: downloading golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
go: downloading google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013
go: downloading golang.org/x/net v0.0.0-20200822124328-c89045814202
go: downloading golang.org/x/text v0.3.0
2021/07/25 00:54:56 RECV: Yamada
2021/07/25 00:54:56 SEND: Hello, Yamada!
# go run ./client/main.go
2021/07/25 00:54:56 RECV: Hello, Yamada!