PythonでgRPCのUnary RPC(1リクエスト-1レスポンス)を実装する

PythonでUnary RPCすなわち1リクエスト-1レスポンス形式のgRPCを実装してみる。

gRPCとは

gRPCとは、Remote Procedure Call (RPC) システムのことで、マイクロサービス間の通信を高速に、かつ簡易に記述することができる。
gRPCには、gRPCクライアント(スタブ)とgRPCサーバの2つの役割がある。
gRPCクライアントはgRPCサーバ上のメソッドを、まるでgRPCクライアントのローカルオブジェクトのメソッドを呼び出しているかのように扱うことができる。

gRPCによる通信はサーバー間だけでなく、gRPC-Web対応のプロキシを経由することでブラウザ、サーバー間の通信にも使うことができる。

Unary RPC

gRPCクライアントがgRPCサーバのメソッドを呼び出す方法は4つあり、それぞれUnary RPC、Server streaming RPC、Client streaming RPC、Bidirectional streaming RPCと呼ばれている。
この記事で実装するUnary RPCは、gRPCクライアントが1つのリクエストに対して1つのレスポンスを受け取る最もシンプルな通信方法だ。

PythonでUnary RPCを開発する手順

  1. .protoファイルにサービスを定義する
  2. grpcio-toolsを使用してメッセージおよびサーバとクライアントのコードを生成する
  3. 生成されたコードを使ってサーバとクライアントのやりとりを実装する

protocol bufferをさわったことがない方は、protocol bufferのみをPythonで操作する記事 PythonでProtocol Buffersの文字列と数値の単純なメッセージを操作するを書いているので参考にしていただけたらと思う。

.protoファイルにサービスを定義する

.protoファイルにサービス、リクエストのメッセージ、レスポンスのメッセージを定義する。

サービスの定義は、serviceキーワードに続けてサービス名を指定する。

service [サービス名] {
  // サービスのメソッド定義
}

サービスのメソッドは、サービスの{}内に記述していく。
rpcキーワードに続きメソッド名を指定する。そして()内にリクエストのメッセージを指定し、returnsキーワードに続き、()内にレスポンスのメッセージを指定する。

rpc [メソッド名]([リクエストメッセージ]) returns ([レスポンスメッセージ]){
}

リクエストメッセージ、レスポンスメッセージは同じフォーマットで、messageキーワードに続きメッセージ名を指定する。 メッセージ内の各フィールドは型名、フィールド名、番号を指定する。

message [メッセージ名] {
  [型] [フィールド名] = [番号(基本的にはフィールドごとに1から連番)];
}

名前を渡したら、「こんにちは, [name]」とメッセージを返すサービスを作成するには次のようにする。
protoファイルにはnameがリクエストとして送られてくること、そして、messageをレスポンスとして返すことしか定義せず、サーバとクライアント間のインターフェースしか定義しない。
実際のロジックは出力されたPythonコードを読み込んで別途実装する。

hello.proto

syntax = "proto3";

package hello;

rpc HelloService {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

grpcio-toolsを使用してメッセージおよびサーバとクライアントのコードを生成する

コードを生成するために、pipenv(あるいはpip)でgrpciogrpcio-toolsをインストールする。

$ touch Pipfile
$ ipenv install --python 3.7.3
$ pipenv install grpcio
$ pipenv install --dev grpcio-tools

pipenv graphコマンドでインストールしたバージョンを確認する。

$ pipenv graph
grpcio-tools==1.21.1
  - grpcio [required: >=1.21.1, installed: 1.21.1]
    - six [required: >=1.5.2, installed: 1.12.0]
  - protobuf [required: >=3.5.0.post1, installed: 3.8.0]
    - setuptools [required: Any, installed: 41.0.1]
    - six [required: >=1.9, installed: 1.12.0]

grpcio1.21.1grpcio-toolsも同じく1.21.1をインストールしている。
またgrpcio-toolsと依存関係のあるprotobuf3.8.0がインストールされている。
protobuf.protoファイルからメッセージのみを出力するが、それに対してgrpcio-tools.protoファイルからメッセージだけではなくクライアント、サーバーも生成する。

次のようなディレクトリ下で.protoファイルからPythonのコードを生成する。

.
├── Pipfile
├── Pipfile.lock
└── hello.proto

.protoファイルからPythonのコードを生成するためのコマンドは次の通り。
python -m grpc.tools.protocに続けて以下の項目を指定する。
-Iでprotoファイルが格納されているディレクトリを指定する。--python_out_pb2.pyがつくファイルを出力するディレクトリを指定する。--grpc_python_out_pb2_grpc.pyがつくファイルを出力するディレクトリを指定する。最後にprotoファイルのパスを指定する。

python -m grpc.tools.protoc -I[protoファイルが格納されているディレクトリ] --python_out=[_pb2.pyがつくファイルを出力するディレクトリ] --grpc_python_out=[_pb2_grpc.pyがつくファイルを出力するディレクトリ] [protoファイル]

hello.protoファイルを入力として、すべてカレントディレクトリに出力する場合は以下の通りにコマンドを実行する。

$ pipenv shell
$ python -m grpc.tools.protoc -I. --python_out=. --grpc_python_out=. ./hello.proto

コマンドを実行すると、hello_pb2.pyhello_pb2_grpc.pyが出力される。





 
 

.
├── Pipfile
├── Pipfile.lock
├── hello.proto
├── hello_pb2.py
└── hello_pb2_grpc.py

hello_pb2.pyにはリクエスト、およびレスポンスメッセージのクラスが含まれており、hello_pb2_grpc.pyにはクライアントおよびサーバのクラスが含まれている。

生成されたコードを使ってサーバとクライアントのやりとりを実装する

生成されたコードをインポートしてサーバとクライアントのコードを実装する。
まずはサーバのコードを実装する。
[サービス名]Servicerを継承したクラスを作成し、サーバのメソッドを実装する。
SayHelloメソッドの第二引数のrequestHelloRequestとして定義したメッセージを受け取ることができる。 そして、戻り値はHelloReplyメッセージを返すようにする。

hello_server.py

import hello_pb2
import hello_pb2_grpc

class HelloService(hello_pb2_grpc.HelloServiceServicer):
    def SayHello(self, request, context):
        return hello_pb2.HelloReply(message=f'こんにちは, {request.name}')

以下のコードを参考にgrpcサーバを立ち上げる部分を実装する。
https://github.com/grpc/grpc/blob/master/examples/python/helloworld/greeter_server.py

from concurrent import futures
import time

import grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    hello_pb2_grpc.add_HelloServiceServicer_to_server(HelloService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    serve()

次にクライアント側のコードを実装する。
サーバと同じように以下のコードを参考にgrpcサーバを立ち上げる部分を実装する。
https://github.com/grpc/grpc/blob/master/examples/python/helloworld/greeter_client.py
HelloServiceStubのgRPCクライアント(スタブ)からメソッドを呼び出す。
リクエスト時はHelloRequestメッセージを指定する。

hello_client.py

import grpc

import hello_pb2
import hello_pb2_grpc


def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = hello_pb2_grpc.HelloServiceStub(channel)
        response = stub.SayHello(hello_pb2.HelloRequest(name='なんしー'))
        print("client received: " + response.message)


if __name__ == '__main__':
    run()

サーバー、クライアントのコードをそれぞれ別のターミナルで立ち上げる。

$ python hello_server.py

クライアントのコードを実行すると、gRPCサーバーからメッセージを受け取っていることが確認できる。

$ $ python hello_client.py
client received: こんにちは, なんしー

・参考
https://grpc.io/docs/guides/concepts/