こんにちは、バックエンドエンジニアのまるです。
この記事では、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方式でデータを送りたいので、UploadRequest
にstream
を付けているのがポイントです。このキーワード1つだけで通信方式をUnaryからClient streamingに変えることができます。逆に、UploadResponse
にstream
を付けるとServer streamingになりますし、両方にstream
を付けるとBidirectional streamingになります。簡単ですね。
これをprotoc
でコンパイルし、file_upload_pb2.py
とfile_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_grpc
のFileUploadServiceStub
クラスを使って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などの動画ファイルの場合でも同様の手法が使えます。ぜひ試してみてください。
<おすすめ記事・メディア>