こんにちは、バックエンドエンジニアのまるです。
この記事では、gRPCのClient streaming方式を使って大容量ファイルを送信する方法を、Pythonの実装例と共にご紹介します。

gRPCとは

gRPCとは、RPC(Remote Procedure Call)フレームワークの一つです。あるサービスから別のサービスのアプリケーションの処理を呼び出すことができます。

アプリケーション間のデータ通信を行うために使われるという点で、WebAPIによく使われるRESTとよく似ています。ただし、思想的な違いがあり、RESTは「リソース志向」という側面が強いのに対し、gRPCは「遠隔呼び出し」を中心に据えています。

gRPCのデータサイズ上限

gRPCのリクエスト/レスポンスのデータサイズには上限が存在し、既定値は4MBです。上限を変更することも可能ですが、「一般的に1MBを超えるメッセージを扱う場合は、別の方法を検討すべき」という記載が公式ドキュメントにあります。

文字列データや小さめの画像であればこの上限はさほど問題になりませんが、動画などは簡単に4MBを超えてしまいます。

そこで、この記事では、Client streamingという方式でリクエストを分割し、1リクエスト4MBの制限を超えずに大きなデータサイズのファイルを送信します。

gRPCの4つの通信方式

gRPCには以下の4種類の通信方式があります。

UnaryRPC

クライアントから送られた1つのレスポンスに対して、サーバーが1つのレスポンスを返して終了する、最もシンプルな形式です。動作はREST APIと似ています。

Server streaming RPC

クライアントから送られたリクエストに対して、サーバーが複数回に分けてレスポンスを返す形式です。時間のかかる処理について、非同期的にレスポンスを返すことができます。

Client streaming RPC

クライアントからリクエストを分割して送り、サーバー側はすべてのリクエストを受け取ってからレスポンスを返します。大きなデータを分割してアップロードしたい場合などに有用です。今回はこの方式を使用します。

Bidirectional streaming RPC

双方向ストリーミング。クライアントから始めのリクエストが送られた後、サーバ・クライアントどちらも任意のタイミングでリクエスト・レスポンスを送ることができます。チャットやゲームのオンライン対戦などに使われます。

.protoファイルの定義

サーバーにファイルを送信する際に、ファイルデータに加えてファイルのメタ情報(ファイル名、説明など)を送信するユースケースが考えられます。

今回は、最初のリクエストでファイルのメタ情報を送信し、2回目以降のリクエストで分割されたファイルのバイト列を送信することにしました。

.protoファイル(file_upload.proto)の定義は以下のようになります。

syntax = "proto3";

package fileupload;

service FileUploadService {
  rpc Upload (stream UploadRequest) returns (UploadResponse);
}

// リクエスト
message UploadRequest {
  // 最初のリクエストはmeta 2回目以降のリクエストはdata
  oneof value {
    Meta meta = 1;
    bytes data = 2;
  }
}

// ファイルメタ情報
message Meta {
  string name = 1;
  string desc = 2;
}

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

Client streaming方式でデータを送りたいので、UploadRequeststreamを付けているのがポイントです。このキーワード1つだけで通信方式をUnaryからClient streamingに変えることができます。逆に、UploadResponsestreamを付けるとServer streamingになりますし、両方にstreamを付けるとBidirectional streamingになります。簡単ですね。

これをprotocでコンパイルし、file_upload_pb2.pyfile_upload_pb2_grpc.pyを生成します。

$ python -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/file_upload.proto

サーバー側の実装

サーバー側の実装(file_upload_server.py)は以下のようになります。

from concurrent import futures

import grpc

from file_upload_pb2 import UploadResponse
from file_upload_pb2_grpc import FileUploadServiceServicer
from file_upload_pb2_grpc import add_FileUploadServiceServicer_to_server

class FileUploadService(FileUploadServiceServicer):

    def Upload(self, request_iterator, context):
        bytearray_data = bytearray()
        for request in request_iterator:
            # メタ情報が送られてきたらコンソールに出力
            if request.meta.name:
                print(request.meta.name)
                print(request.meta.desc)
                continue
            # データが送られてきたらバイト列に追加
            bytearray_data.extend(request.data)

        # バイト列をファイルに書き込む
        data = bytes(bytearray_data)
        with open("cat_sent.jpg", mode="wb") as f:
            f.write(data)

        # レスポンスを返す
        return UploadResponse(result="sucess!!")

class Server():
    def __init__(self):
        self.file_upload_server = FileUploadService()

    def start(self):

        self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
        add_FileUploadServiceServicer_to_server(
            self.file_upload_server, self.server
        )

        self.server.add_insecure_port("[::]:50051")
        self.server.start()
        print("Starting gRPC file_upload server...")
        self.server.wait_for_termination()

if __name__ == "__main__":
    server = Server()
    server.start()

主な処理はUpload()で行われています。ファイルのメタ情報が送られてきたらコンソールに出力し、その後ファイルのデータが送られてくるたびにバイト列に追加して、リクエストが終わったらファイルにバイト列を書き込み、最後にレスポンスを返して終了します。

サーバーの立て方についてはここでは解説しませんので、公式のBasics Tutorialを参考にしてください。

クライアント側の実装

クライアント側の実装(file_upload_client.py)は以下のようになります。

※可愛い猫の画像は各自でご用意ください。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import grpc

import file_upload_pb2
import file_upload_pb2_grpc

def read_iterfile(filepath, chunk_size=1024):
    # ファイルのメタ情報を送信
    meta = file_upload_pb2.Meta(
        name="cat.jpg",
        desc="可愛い猫の画像です"
    )
    yield file_upload_pb2.UploadRequest(meta=meta)

    # ファイルデータをchunk_sizeごとに分割して送信
    with open(filepath, mode="rb") as f:
        while True:
            data = f.read(chunk_size)
            if data:
                entry_request = file_upload_pb2.UploadRequest(
                    data=data)
                yield entry_request
            else:
                return

def run():
    with grpc.insecure_channel("localhost:50051") as channel:
        stub = file_upload_pb2_grpc.FileUploadServiceStub(channel)
        response = stub.Upload(read_iterfile("cat.jpg"))
        print("Client received:", response, sep='\\n')

if __name__ == "__main__":
    run()

file_upload_pb2_grpcFileUploadServiceStubクラスを使ってstubを作成し、Uploadメソッドを呼び出します。

処理としては、最初にファイルのメタ情報を送信した後、cat.jpgというパスのファイルを1024バイト(1KB)ごとに読み込み、サーバーに送信します。

例としてファイルの分割単位を1KBにしていますが、他の値でも問題ありません。

実行

初めにサーバーを実行します。

$ python -m file_upload_server
Starting gRPC file_upload server...

ログが出力され、サーバーがリクエスト待ちの状態になります。

次に、別のコンソールを開いてクライアントを実行します。

$ python -m file_upload_client
Client received:
result: "sucess!!"

result: "sucess!!"が返ってきたら成功です。送信した画像ファイルと同じ内容のcat_sent.jpgという名前のファイルが作成されているはずです。

また、サーバー側の標準出力を見ると、ファイルのメタ情報が送られていることが確認できます。

$ python -m file_upload_server
Starting gRPC file_upload server...
cat.jpg
可愛い猫の画像です

お疲れ様でした。

おわりに

gRPCのClient streaming方式を使って大容量ファイルを送信する方法をご紹介しました。
今回の例では.jpgファイルを用いましたが、.mp4などの動画ファイルの場合でも同様の手法が使えます。ぜひ試してみてください。

<おすすめ記事・メディア>

gRPCとProtocol Buffersについて復習するためのリンク集

SHARE

  • facebook
  • twitter

SQRIPTER

AGEST Engineers

AGEST

記事一覧

AGESTのエンジニアが情報発信してます!

株式会社AGEST

Sqriptsはシステム開発における品質(Quality)を中心に、エンジニアが”理解しやすい”Scriptに変換して情報発信するメディアです

  • 新規登録/ログイン
  • 株式会社AGEST