PythonでgRPCについて仕組みを学ぼう

スポンサーリンク

はじめに

gRPCとは、Googleが開発した高性能なオープンソースのRPC(Remote Procedure Call)フレームワークで、異なるプログラムやサービス間で効率的かつ高速な通信を実現します。

gRPCでは以下のメリットがあります。

  • 高速な通信
    データをバイナリ化して転送するためJSONなどに比べて軽量
  • コード自動生成
    .protoファイル1つを定義するだけで各言語に対応したひな形を生成
  • 型安全
    通信内容の型が厳密に定義され、開発効率と信頼性を向上

「gRPCは学習コストが高そう…」と感じるかもしれませんが、一度流れを掴めば非常に強力な武器になります。今回はPythonを使って、その「雰囲気」を掴むためのハンズオンを行います。

目標

「サーバにあるユーザ情報をクライアント側からのリクエストに応じて取得・表示する」というシンプルなプログラムを作成します。

環境を整える

まずはPythonの開発環境にてgRPCを使えるようにパッケージをインストールします。

pip install grpcio grpcio-tools

フォルダ・ファイル階層

今回は、動作を学習する目的のため同一フォルダにサーバとクライアントを配置します。

/
┗src/
 ┣pb/            # 自動生成コードの格納先
 ┣proto/         # 定義ファイルの格納先
 ┃┗user.proto
 ┣client.py      # クライアントの実装
 ┗server.py      # サーバの実装

Protocol Buffers(定義ファイル)の作成

まずは「どんなデータを、どうやり取りするか」を定義する.protoファイルを作成します。このファイルでは実際に使用するサービスやリクエスト・レスポンスのデータ構造を定義します。

syntax = "proto3";
package user;

// サービス名(メソッド名)の定義
service UserService {
    rpc GetUser (UserRequest) returns (UserResponse);
}

// リクエストの定義
message UserRequest {
    int32 section = 1;  // 部署IDを想定
}

// レスポンスの定義
message UserResponse {
    repeated User users = 1;  // User型のリスト
}

// データ単体の定義(ここではUser型のリスト項目)
message User {
    int32 id = 1;
    string name = 2;
    int32 section = 3;
}

コード自動生成

以下のコマンドを実行して、Pythonから利用できるモジュールを/src/pb/フォルダ配下に生成します。

cd src
python -m grpc_tools.protoc -I./proto --python_out=./pb --pyi_out=./pb --grpc_python_out=./pb ./proto/user.proto

実行後、pb/フォルダに「user_pb2.py(データ定義用)」と「user_pb2_grpc.py(通信定義用)」が生成されていれば成功です。

サーバ側の実装

サーバ側では、定義したUserServiceの中身を記載します。

from concurrent import futures
import grpc
from pb import user_pb2
from pb import user_pb2_grpc

# サンプルデータ(本来はDBなどから取得)
USERS = [
    {"id": 1, "name": "山田太郎", "section": 11},
    {"id": 2, "name": "鈴木宗助", "section": 12},
    {"id": 3, "name": "織田信介", "section": 11},
    {"id": 4, "name": "多田次郎", "section": 11},
    {"id": 5, "name": "森山卓司", "section": 12},
]

# サービスロジックを実装するクラス
class User(user_pb2_grpc.UserServiceServicer):
    def GetUser(self, request, context):
        datas = []
        section = request.section

        for user in USERS:
            if user["section"] == section:
                datas.append(
                    user_pb2.User(
                        id=user["id"],
                        name=user["name"],
                        section=user["section"],
                    )
                )
        return user_pb2.UserResponse(users=datas)


def serve():
    # サーバ起動設定
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    user_pb2_grpc.add_UserServiceServicer_to_server(User(), server)

    # 5001ポートで待ち受け
    server.add_insecure_port("[::]:5001")
    print("Server started on port 5001...")
    server.start()
    server.wait_for_termination()
    

if __name__ == "__main__":
    serve()

クライアント側

サーバに対してリクエストを送り、結果を表示します。

import grpc
from pb import user_pb2
from pb import user_pb2_grpc

def run():
    # サーバへ接続(今回はlocalhostへ)
    with grpc.insecure_channel('localhost:5001') as channel:
        # 接続用の窓口(Stub)を作成
        stub = user_pb2_grpc.UserServiceStub(channel)

        # リクエストの作成(section 11を指定)
        request = user_pb2.UserRequest(section=11)

        # サーバーのメソッドを呼び出し
        response = stub.GetUser(request)

    # 結果の表示
    for res in response.users:
        print("--------")
        print("id %d, name %s, section %d" % (res.id, res.name, res.section))

if __name__ == '__main__':
    run()

実行結果

ターミナルを二つ開いて以下を実行します。

サーバの起動

python server.py
# Server started on port 5001...と表示される

クライアントの実行

python client.py

出力結果:


--------
id 1, name 山田太郎, section 11
--------
id 3, name 織田信介, section 11
--------
id 4, name 多田次郎, section 11

クライアント側で指定した条件に合わせて出たが返ってくれば成功です。

おわりに

gRPCを使うことで、まるでローカルの関数を呼び出すような感覚で、別プロセスのサーバーと通信できることが実感できたでしょうか?

今回は同一PC内での通信でしたが、サーバーを別のマシンやクラウド、あるいは別のプログラミング言語(GoやJavaなど)で実装しても、同じ .proto ファイルさえあれば簡単に通信が可能です。ぜひ、より複雑なデータ構造や双方向通信(Streaming)にも挑戦してみてください。

コメント

タイトルとURLをコピーしました