gRPC-Python 使用入门

1. 简介

在此 Codelab 中,您将使用 gRPC-Python 创建一个客户端和服务器,它们将构成以 Python 编写的路线映射应用的基础。

在本教程结束时,您将拥有一个使用 gRPC 连接到远程服务器的客户端,以获取地图上特定坐标处的位置名称或邮寄地址。一个功能完善的应用可能会使用这种客户端-服务器设计来枚举或总结路线沿途的兴趣点。

该服务在 Protocol Buffers 文件中定义,该文件将用于为客户端和服务器生成样板代码,以便它们能够相互通信,从而节省您实现该功能的时间和精力。

生成的代码不仅能处理服务器与客户端之间复杂的通信,还能处理数据序列化和反序列化。

学习内容

  • 如何使用 Protocol Buffers 定义服务 API。
  • 如何使用自动代码生成功能基于 Protocol Buffers 定义构建基于 gRPC 的客户端和服务器。
  • 了解使用 gRPC 进行客户端-服务器通信。

此 Codelab 适合刚开始使用 gRPC 或希望复习 gRPC 的 Python 开发者,也适合任何对构建分布式系统感兴趣的人。无需具备 gRPC 经验。

2. 准备工作

所需条件

  • Python 3.9 或更高版本。我们建议使用 Python 3.13。如需查看针对具体平台的安装说明,请参阅 Python 设置和使用。或者,使用 uv pyenv 等工具安装非系统 Python。
  • 使用 pip 安装 Python 软件包。
  • venv 来创建 Python 虚拟环境。

ensurepipvenv 软件包是 Python 标准库的一部分,通常默认可用。

不过,一些基于 Debian 的发行版(包括 Ubuntu)在重新分发 Python 时选择排除这些文件。如需安装软件包,请运行以下命令:

sudo apt install python3-pip python3-venv

获取代码

为了简化学习过程,此 Codelab 提供了预构建的源代码框架,可帮助您快速入门。以下步骤将引导您完成应用,包括使用 grpc_tools.protoc Protocol Buffer 编译器插件生成 gRPC 代码。

grpc-codelabs

此 Codelab 的框架源代码位于 codelabs/grpc-python-getting-started/start_here 目录中。如果您不想自己实现代码,可以在 completed 目录中找到已完成的源代码。

首先,创建 Codelab 工作目录并进入该目录:

mkdir grpc-python-getting-started && cd grpc-python-getting-started

下载并解压缩 Codelab:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-python-getting-started/start_here

或者,您也可以下载仅包含 Codelab 目录的 .zip 文件,然后手动将其解压缩。

3. 定义服务

第一步是使用 Protocol Buffers 接口定义语言定义应用的 gRPC 服务、其 RPC 方法以及其请求和响应消息类型。您的服务将提供:

  • 一种名为 GetFeature 的 RPC 方法,由服务器实现并由客户端调用。
  • 消息类型 PointFeature 是使用 GetFeature 方法时在客户端和服务器之间交换的数据结构。客户端在其 GetFeature 请求中向服务器提供地图坐标作为 Point,服务器则会回复相应的 Feature,其中描述了位于这些坐标处的任何内容。

此 RPC 方法及其消息类型都将在所提供源代码的 protos/route_guide.proto 文件中定义。

Protocol Buffers 通常称为 protobuf。如需详细了解 gRPC 术语,请参阅 gRPC 的核心概念、架构和生命周期

消息类型

在源代码的 protos/route_guide.proto 文件中,首先定义 Point 消息类型。Point 表示地图上的纬度-经度坐标对。在此 Codelab 中,请使用整数作为坐标:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

数字 12message 结构中每个字段的唯一 ID 编号。

接下来,定义 Feature 消息类型。Feature 使用 string 字段来表示由 Point 指定的位置处的某个事物的名称或邮政地址:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

服务方法

route_guide.proto 文件具有名为 RouteGuideservice 结构,用于定义应用服务提供的一个或多个方法。

RouteGuide 定义中添加 rpc 方法 GetFeature。如前所述,此方法将根据给定的坐标集查找某个位置的名称或地址,因此让 GetFeature 为给定的 Point 返回 Feature

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

这是一元 RPC 方法:一种简单 RPC,其中客户端向服务器发送请求并等待响应返回,就像本地函数调用一样。

4. 生成客户端和服务器代码

接下来,使用协议缓冲区编译器从 .proto 文件中为客户端和服务器生成样板 gRPC 代码。

对于 gRPC Python 代码生成,我们创建了 grpcio-tools。其中包括:

  1. 常规的 protoc 编译器,用于根据 message 定义生成 Python 代码。
  2. gRPC protobuf 插件,可根据 service 定义生成 Python 代码(客户端和服务器桩)。

我们将使用 pip 安装 grpcio-tools Python 软件包。让我们创建一个新的 Python 虚拟环境 (venv),以将项目的依赖项与系统软件包隔离开来:

python3 -m venv --upgrade-deps .venv

在 bash/zsh shell 中激活虚拟环境:

source .venv/bin/activate

对于 Windows 和非标准 shell,请参阅 https://docs.python.org/3/library/venv.html#how-venvs-work 中的表格。

接下来,安装 grpcio-tools(这也会安装 grpcio 软件包):

pip install grpcio-tools

使用以下命令生成 Python 样板代码:

python -m grpc_tools.protoc --proto_path=./protos  \
 --python_out=. --pyi_out=. --grpc_python_out=. \
 ./protos/route_guide.proto

这将为我们在 route_guide.proto 中定义的接口生成以下文件:

  1. route_guide_pb2.py 包含根据 message 定义动态创建类的代码。
  2. route_guide_pb2.pyi 是根据 message 定义生成的 “桩文件”或“类型提示文件”。它仅包含签名,不包含实现。IDE 可以使用桩文件来提供更好的自动补全和错误检测功能。
  3. route_guide_pb2_grpc.py 是根据 service 定义生成的,包含特定于 gRPC 的类和函数。

gRPC 专用代码包含:

  1. RouteGuideStubgRPC 客户端可以使用它来调用 RouteGuide RPC。
  2. RouteGuideServicer,用于定义 RouteGuide 服务实现的接口。
  3. 用于向 gRPC 服务器注册 RouteGuideServiceradd_RouteGuideServicer_to_server 函数。

5. 创建服务

首先,我们来看看如何创建 RouteGuide 服务器。创建和运行 RouteGuide 服务器分为两个工作项:

  • 实现从服务定义生成的服务接口,其中包含执行服务实际“工作”的函数。
  • 在特定端口运行 gRPC 服务器,以监听来自客户端的请求并传输响应。

您可以在 start_here/route_guide_server.py 中找到初始 RouteGuide 服务器。

实现 RouteGuide

route_guide_server.py 具有一个 RouteGuideServicer 类,该类是生成的类 route_guide_pb2_grpc.RouteGuideServicer 的子类:

# RouteGuideServicer provides an implementation
# of the methods of the RouteGuide service.
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):

RouteGuideServicer 实现所有 RouteGuide 服务方法。

下面我们来详细了解一下简单的 RPC 实现。方法 GetFeature 从客户端获取 Point,并从其数据库中返回 Feature 中的相应功能信息。

def GetFeature(self, request, context):
    feature = get_feature(self.db, request)
    if feature is None:
        return route_guide_pb2.Feature(name="", location=request)
    else:
        return feature

该方法会传递一个针对 RPC 的 route_guide_pb2.Point 请求,以及一个提供 RPC 特定信息(例如超时限制)的 grpc.ServicerContext 对象。它会返回 route_guide_pb2.Feature 响应。

启动服务器

实现所有 RouteGuide 方法后,下一步是启动 gRPC 服务器,以便客户端能够实际使用您的服务:

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
        RouteGuideServicer(),
        server,
    )
    listen_addr = "localhost:50051"
    server.add_insecure_port(listen_addr)
    print(f"Starting server on {listen_addr}")
    server.start()
    server.wait_for_termination()

服务器 start() 方法是非阻塞的。系统将实例化一个新线程来处理请求。调用 server.start() 的线程通常在此期间不会有任何其他工作要做。在这种情况下,您可以调用 server.wait_for_termination() 来干净地阻塞调用线程,直到服务器终止。

6. 创建客户端

在本部分中,我们将了解如何为 RouteGuide 服务创建客户端。您可以在 start_here/route_guide_client.py 中查看初始客户端代码。

创建桩

如需调用服务方法,我们首先需要创建 stub

我们实例化了 route_guide_client.py 文件中由 .proto 生成的 route_guide_pb2_grpc 模块的 RouteGuideStub 类。

channel = grpc.insecure_channel("localhost:50051")
stub = route_guide_pb2_grpc.RouteGuideStub(channel)

调用服务方法

对于返回单个响应的 RPC 方法(称为“response-unary”方法),gRPC Python 同时支持同步(阻塞)和异步(非阻塞)控制流语义。

简单 RPC

首先,我们来定义一个 Point 以调用该服务。这应该很简单,只需使用一些属性从 route_guide_pb2 软件包中实例化一个对象即可:

point = route_guide_pb2.Point(latitude=412346009, longitude=-744026814)

对简单 RPC GetFeature 的同步调用几乎与调用本地方法一样简单。RPC 调用会等待服务器响应,并返回响应或引发异常。我们可以调用该方法并查看如下所示的响应:

feature = stub.GetFeature(point)
print(feature)

您可以检查 Feature 对象的字段并输出请求的结果:

if feature.name:
    print(f"Feature called '{feature.name}' at {format_point(feature.location)}")
else:
    print(f"Found no feature at at {format_point(feature.location)}")

7. 试试看

运行服务器:

python route_guide_server.py

在另一个终端中,再次激活虚拟环境,然后运行客户端:

python route_guide_client.py

您将看到如下输出(为清晰起见,省略了时间戳):

name: "16 Old Brook Lane, Warwick, NY 10990, USA"
location {
  latitude: 412346009
  longitude: -744026814
}

Feature called '16 Old Brook Lane, Warwick, NY 10990, USA' at latitude: 412346009, longitude: -744026814

8. 后续步骤