Python 中的 gRPC 文件上传和下载

通过阅读本文,您将学习如何设置自己的 gRPC 客户端和服务器以使用 Python 上传/下载文件。供您参考,gRPC 被称为远程过程调用,这是一种现代开放源代码,用于将设备、移动应用程序和浏览器连接到后端服务。

它的核心具有以下功能:

  • simple service definition— 通过 Protocol Buffers 定义,一个强大的二进制序列化工具
  • scalable— 扩展到每秒数百万个 RPC
  • multi-platform advantage— 跨不同语言和平台工作
  • streaming support— 支持客户端和服务器之间的双向流

本教程涵盖以下概念和实现:

  • 安装 Python 模块和 gRPC 工具
  • 通过 Protocol Buffers 定义服务
  • 生成 gRPC 代码
  • 搭建gRPC服务器用于文件上传下载
  • 构建一个 gRPC 客户端来从服务器上传和下载文件
  • 流媒体更新服务
  • 更新 gRPC 服务器以进行文件上传/下载
  • 更新 gRPC 客户端以进行文件上传/下载

让我们继续下一节的设置和安装

 

设置

强烈建议在继续安装之前设置虚拟环境。

在终端上运行以下命令进行安装grpcio


pip install grpcio

除此之外,我们还需要安装protocol buffer编译器自带的gRPC工具来生成服务端和客户端代码。您可以按如下方式安装它:

pip install grpcio-tools
 

定义服务

protos在您的工作目录中创建一个名为的新文件夹。然后,创建一个名为hello.proto该文件作为整个项目的服务定义(适用于客户端和服务器)。

我们将定义数据序列化的结构。

  • message— 表示包含一系列名称-值对的信息的逻辑记录。每个项目通常称为字段。您必须定义字段类型、字段名称和字段编号。字段编号作为唯一标识符,不应更改。它从 1 开始,1 到 15 范围内的数字需要一个字节来编码。另一方面,16 到 2047 范围内的数字需要两个字节进行编码。

看看下面的例子:


message HelloRequest {
string name = 1;
int32 age = 2;
}message StringResponse {
string message = 1;
}

HelloRequest消息包含一个字符串和 int32 变量。您可以将它用于请求或响应,因为没有限制。在这种情况下,我们将把它用作请求,这意味着传入的请求应该有一个name字符串类型的age变量和一个 int32 类型的变量。

另一方面,StringResponse 消息只包含一个消息变量。因此,我们的 gRPC 服务将接受两个输入变量(string、int32)并返回一个变量(string)作为响应。

  • service — 代表服务定义。每个服务可以有多个具有自己的请求和响应定义的 rpc。

以下代码片段说明了一个简单的服务定义,它利用来自

The following code snippet illustrates a simple service definition, which utilizes the messages from the

service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (StringResponse) {}
}

Greeter服务包含一个SayHello接受HelloRequest消息和返回StringResponse消息的 rpc。

无论您使用何种平台和编程语言,Protocol Buffers 定义都是通用的。查看语言指南 (proto3)以获取有关 Protocol Buffers 的更多信息。

看看下面的原型文件作为参考:

 
 
syntax = "proto3";

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (StringResponse) {}
}

message HelloRequest {
  string name = 1;
  int32 age = 2;
}

message StringResponse {
  string message = 1;
}

 

 

生成 gRPC 代码

下一步是生成相应的 Python 代码以供客户端和服务器使用。假设你的工作目录如下:

root (working directory)
|
|--protos
| |-hello.proto
|--client.py (client code, will be created in the later section)
|--server.py (server code, will be created in the later section)

Call the following command from your working directory:

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

It will generate the following files in the protos folder:

  • hello_pb2.py
  • hello_pb2_grpc.py

文件的前缀取决于 *.proto 文件的名称。每当对 proto 文件进行修改时,都需要重新生成 gRPC 代码。

 

gRPC 服务器

server.py在您的工作目录中创建一个名为的新文件。

打开它并添加以下导入语句:

from concurrent import futures
import logging
import os
import grpc
from protos import hello_pb2, hello_pb2_grpc

然后,Greeter使用名为的函数定义类SayHello

Then, define the Greeter class with a function called SayHello:

class Greeter(hello_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
return hello_pb2.StringResponse(message=f'Hello, {request.name}! Your age is {request.age}')

Inside the function, you can implement the desired logic and return StringResponse message back to the client.

The name of the function should matched with what we have defined earlier in the proto file.

在函数内部,您可以实现所需的逻辑并将StringResponse消息返回给客户端。

函数的名称应该与我们之前在proto文件中定义的名称相匹配。

之后,定义一个名为 serve 的新函数并在其中附加以下代码:

def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()

它将在端口 50051 上运行带有 4 个 worker 的服务器。

最后在文件底部添加以下代码:

if __name__ == '__main__':
logging.basicConfig()
serve()

完整的服务器代码:

from concurrent import futures
import logging
import os
import grpc
from protos import hello_pb2, hello_pb2_grpc

class Greeter(hello_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return hello_pb2.StringResponse(message=f'Hello, {request.name}! Your age is {request.age}')


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
    hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    logging.basicConfig()
    serve()

 

在您的工作目录上运行以下命令:

python server.py
 

gRPC 客户端

完成服务器代码后,创建一个名为client.py.

在文件顶部添加以下导入语句:

import logging
import os
import grpc
from protos import hello_pb2, hello_pb2_grpc

继续添加一个run使用以下代码调用的新函数:

def run():
with grpc.insecure_channel('localhost:50051') as channel:
stub = hello_pb2_grpc.GreeterStub(channel)
response = stub.SayHello(hello_pb2.HelloRequest(name='John Doe', age=30))
print("Greeter client received: " + response.message)

It will connect to the gRPC server at port 50051 and call the SayHello rpc with the following input:

  • name — John Doe
  • age — 30

Add the following code at the end of the client.py script:

if __name__ == '__main__':
logging.basicConfig()
run()

请查看以下代码片段作为参考:

 

import logging
import os
import grpc
from protos import hello_pb2, hello_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = hello_pb2_grpc.GreeterStub(channel)
        response = stub.SayHello(hello_pb2.HelloRequest(name='John Doe', age=30))
        print("Greeter client received: " + response.message)
        
if __name__ == '__main__':
    logging.basicConfig()
    run()

 

确保您当前正在运行server.py打开一个新终端并运行以下命令:

python client.py

You should see the following output:

Greeter client received: Hello, John Doe! Your age is 30
 

流媒体更新服务

对于文件响应,最好的方法是通过流返回字节。您可以按如下方式定义消息:

message FileResponse {
bytes chunk_data = 1;
}

至于文件请求,最好先创建一条MetaData消息,然后再将其用作消息的自定义字段类型。


message MetaData {
string filename = 1;
string extension = 2;
}

这允许您传递图像/音频/视频/文件数据以及其他重要信息。由于流式传输将在文件末尾间隔调用,因此我们可以避免通过oneof关键字发送相同的信息。

oneof字段与普通字段类似,只是所有字段都在一个oneof共享内存中,最多可以同时设置一个字段。


message UploadFileRequest {
  oneof request {
    MetaData metadata = 1;
    bytes chunk_data = 2;
  }
}

 

在这种情况下,我们可以先发送Metadata消息,然后再将文件流式传输到 gRPC 服务器。

我们需要用两个新的 rpc 更新 Greeter 服务:

  • 上传文件
  • 下载文件
service Greeter {
 rpc SayHello (HelloRequest) returns (StringResponse) {}
 rpc UploadFile(stream UploadFileRequest) returns (StringResponse) {}
 rpc DownloadFile(MetaData) returns (stream FileResponse) {}
}

在流媒体功能的请求或响应消息的前面添加stream关键字。

您可以hello.proto在以下要点找到完整的内容:

syntax = "proto3";

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (StringResponse) {}
  // Uploads a file
  rpc UploadFile(stream UploadFileRequest) returns (StringResponse) {}
  // Downloads a file
  rpc DownloadFile(MetaData) returns (stream FileResponse) {}
}

message HelloRequest {
  string name = 1;
  int32 age = 2;
}

message StringResponse {
  string message = 1;
}

message FileResponse {
  bytes chunk_data = 1;
}

message MetaData {
  string filename = 1;
  string extension = 2;
}

message UploadFileRequest {
  oneof request {
    MetaData metadata = 1;
    bytes chunk_data = 2;
  }
}

请记住运行以下命令以重新生成 gRPC 代码:


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

更新 gRPC 服务器

server.py使用服务下的两个新功能修改文件Greeter

  • UploadFile
  • DownloadFile

SayHello函数不同,输入参数UploadFile是一个迭代器,因为我们已将其指定为流式请求。

def get_filepath(filename, extension):
    return f'{filename}{extension}'


class Greeter(hello_pb2_grpc.GreeterServicer):
    ....

    def UploadFile(self, request_iterator, context):
        data = bytearray()
        filepath = 'dummy'

        for request in request_iterator:
            if request.metadata.filename and request.metadata.extension:
                filepath = get_filepath(request.metadata.filename, request.metadata.extension)
                continue
            data.extend(request.chunk_data)
        with open(filepath, 'wb') as f:
            f.write(data)
        return hello_pb2.StringResponse(message='Success!')

 

遍历迭代器以获取请求字段。由于我们使用oneof关键字,因此迭代器中的每个请求都可以是 aMetaDatabytes我们可以很容易地使用条件检查来确定内容并进行相应的处理。

在这种情况下,我们会将其存储chunk_data到一个bytearray变量中并将其保存在流的末尾。然后,我们StringResponse向客户端返回一条消息。

同时,该DownloadFile函数会将文件流式传输回客户端。最简单的实现是逐块读取文件并将其作为FileResponse消息返回。

 

class Greeter(hello_pb2_grpc.GreeterServicer):
    def DownloadFile(self, request, context):
        chunk_size = 1024

        filepath = f'{request.filename}{request.extension}'
        if os.path.exists(filepath):
            with open(filepath, mode="rb") as f:
                while True:
                    chunk = f.read(chunk_size)
                    if chunk:
                        entry_response = hello_pb2.FileResponse(chunk_data=chunk)
                        yield entry_response
                    else:  # The chunk was empty, which means we're at the end of the file
                        return

出于演示目的,我已将 chunk_size 设置为 1024。您可以根据您的用例将其设置为更高的值。

with streaming capabilities的完整代码server.py如下:

from concurrent import futures
import logging
import os
import grpc
from protos import hello_pb2, hello_pb2_grpc


def get_filepath(filename, extension):
    return f'{filename}{extension}'


class Greeter(hello_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return hello_pb2.StringResponse(message=f'Hello, {request.name}! Your age is {request.age}')

    def UploadFile(self, request_iterator, context):
        data = bytearray()
        filepath = 'dummy'

        for request in request_iterator:
            if request.metadata.filename and request.metadata.extension:
                filepath = get_filepath(request.metadata.filename, request.metadata.extension)
                continue
            data.extend(request.chunk_data)
        with open(filepath, 'wb') as f:
            f.write(data)
        return hello_pb2.StringResponse(message='Success!')

    def DownloadFile(self, request, context):
        chunk_size = 1024

        filepath = f'{request.filename}{request.extension}'
        if os.path.exists(filepath):
            with open(filepath, mode="rb") as f:
                while True:
                    chunk = f.read(chunk_size)
                    if chunk:
                        entry_response = hello_pb2.FileResponse(chunk_data=chunk)
                        yield entry_response
                    else:  # The chunk was empty, which means we're at the end of the file
                        return


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
    hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    logging.basicConfig()
    serve()

停止服务器并使用以下命令重新运行它:


python server.py
 

更新 gRPC 客户端

让我们更新client.py文件以调用以下函数:

  • 上传文件
  • 下载文件

请注意,UploadFilerpc 接受一个迭代器,因为它是一个流式请求。因此,创建一个新的迭代器调用read_iterfile来逐块读取文件:

 
def read_iterfile(filepath, chunk_size=1024):
    split_data = os.path.splitext(filepath)
    filename = split_data[0]
    extension = split_data[1]

    metadata = hello_pb2.MetaData(filename=filename, extension=extension)
    yield hello_pb2.UploadFileRequest(metadata=metadata)
    with open(filepath, mode="rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if chunk:
                entry_request = hello_pb2.UploadFileRequest(chunk_data=chunk)
                yield entry_request
            else:  # The chunk was empty, which means we're at the end of the file
                return
 

chunk_size出于演示目的,我已将 设置为 1024。根据您的用例修改它。

然后,您可以调用rpc如下:

def run():
    以 grpc.insecure_channel('localhost:50051') 作为通道:
        ...
        response = stub.UploadFile(read_iterfile('test.txt')) 
        print("Greeter client received: " + response.message)
替换test.txt为您的文件路径。

另一方面,DownloadFilerpc 返回流式响应。我们可以轻松地迭代它并逐块保存文件:
def get_filepath(filename, extension):
    return f'{filename}{extension}'
    

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        ...
        
        filename = 'test'
        extension = '.jpg'
        filepath = get_filepath(filename, extension)
        for entry_response in stub.DownloadFile(hello_pb2.MetaData(filename=filename, extension=extension)):
            with open(filepath, mode="ab") as f:
                f.write(entry_response.chunk_data)

  

 
根据您的用例替换filename和变量的值。extension

的完整代码client.py位于以下要点:

 

The complete code for client.py is located at the following gist:

import logging
import os
import grpc
from protos import hello_pb2, hello_pb2_grpc

def get_filepath(filename, extension):
    return f'{filename}{extension}'


def read_iterfile(filepath, chunk_size=1024):
    split_data = os.path.splitext(filepath)
    filename = split_data[0]
    extension = split_data[1]

    metadata = hello_pb2.MetaData(filename=filename, extension=extension)
    yield hello_pb2.UploadFileRequest(metadata=metadata)
    with open(filepath, mode="rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if chunk:
                entry_request = hello_pb2.UploadFileRequest(chunk_data=chunk)
                yield entry_request
            else:  # The chunk was empty, which means we're at the end of the file
                return


def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = hello_pb2_grpc.GreeterStub(channel)
        response = stub.SayHello(hello_pb2.HelloRequest(name='John Doe', age=30))
        print("Greeter client received: " + response.message)

        response = stub.UploadFile(read_iterfile('test.txt'))
        print("Greeter client received: " + response.message)

        filename = 'test'
        extension = '.jpg'
        filepath = get_filepath(filename, extension)
        for entry_response in stub.DownloadFile(hello_pb2.MetaData(filename=filename, extension=extension)):
            with open(filepath, mode="ab") as f:
                f.write(entry_response.chunk_data)


if __name__ == '__main__':
    logging.basicConfig()
    run()

  

Run the following command to test file uploading and downloading via gRPC:
python client.py
 

让我们回顾一下您今天所学的内容。

在继续设置和安装之前,本文首先简要介绍了 gRPC。

然后,它涵盖了通过 Protocol Buffers 进行的消息和服务定义。它还强调了生成 gRPC 代码以供客户端和服务器应用程序使用的步骤。

它还解释了基本的客户端和服务器代码实现。随后,它提供了代码来更新现有的通过流媒体上传和下载文件的代码。

 

References

  1. gRPC Documentation — Quick start
  2. proto3 — Language Guide
  3. https://gist.github.com/wfng92
posted @ 2022-11-29 22:48  DaisyLinux  阅读(971)  评论(0编辑  收藏  举报