基础教程

本教程为 PHP 程序员提供了一个关于如何使用 gRPC 的基础介绍。

通过本示例,你将学习如何:

  • .proto 文件中定义一个服务。

  • 使用协议缓冲编译器生成客户端代码。

  • 使用 PHP gRPC API 为你的服务编写一个简单的客户端。

本教程假设你对 协议缓冲 已有一定了解。请注意,本示例使用的是 proto2 版本的协议缓冲语言。

此外,目前你只能在 PHP 中创建 gRPC 客户端。要创建 gRPC 服务器,需使用其他语言。

为什么使用 gRPC?

我们的示例是一个简单的路由映射应用程序,允许客户端获取关于路由上的特征信息,创建路由摘要,并与服务器和其他客户端交换如交通更新等路由信息。

使用 gRPC,我们可以在 .proto 文件中定义服务一次,然后生成任何 gRPC 支持语言的客户端和服务器,这些客户端和服务器可以在从大型数据中心内的服务器到你自己的平板设备等各种环境中运行——所有不同语言和环境之间的通信复杂性都由 gRPC 为你处理。我们还可以享受使用协议缓冲的所有优势,包括高效的序列化、简单的接口定义语言(IDL)以及轻松的接口更新。

示例代码和设置

本教程的示例代码位于 grpc/grpc/examples/php/route_guide。要下载示例代码,请通过以下命令克隆 gRPC 仓库及其子模块:

git clone --recurse-submodules -b v1.66.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc

你需要 grpc-php-plugin 来帮助你编译 .proto 文件。按照以下步骤从源代码构建它:

cd grpc
mkdir -p cmake/build
pushd cmake/build
cmake ../..
make protoc grpc_php_plugin
popd

然后切换到 route_guide 目录并编译示例的 .proto 文件:

cd examples/php/route_guide
./route_guide_proto_gen.sh

我们的示例是一个简单的路由映射应用程序,允许客户端获取关于路由上的特征信息,创建路由摘要,并与服务器和其他客户端交换如交通更新等路由信息。

你还需要安装相关工具来生成客户端接口代码(以及在其他语言中生成的服务器用于测试)。你可以通过跟随 这些设置说明 来获得它们。

尝试示例!

要尝试示例应用程序,我们需要一个正在本地运行的 gRPC 服务器。让我们编译并运行仓库中的 Node.js 服务器:

cd ../../node
npm install
cd dynamic_codegen/route_guide
nodejs ./route_guide_server.js --db_path=route_guide_db.json

在另一个终端中,运行 PHP 客户端:

./run_route_guide_client.sh

接下来的部分将逐步指导你如何定义该 proto 服务,如何从中生成客户端库,以及如何创建一个使用该库的客户端存根。

定义服务

首先,我们来看一下我们正在使用的服务是如何定义的。一个 gRPC 服务及其方法请求和响应类型是通过 协议缓冲 定义的。你可以在 examples/protos/route_guide.proto 中查看完整的 .proto 文件。

要定义服务,你在 .proto 文件中指定一个命名的服务:

service RouteGuide {
   ...
}

然后,在服务定义中定义 rpc 方法,指定它们的请求和响应类型。协议缓冲让你定义四种类型的服务方法,这些方法都在 RouteGuide 服务中使用:

  • 简单的 RPC:客户端向服务器发送请求,稍后接收响应,就像正常的远程过程调用。

    // 获取给定位置的特征。
    rpc GetFeature(Point) returns (Feature) {}
  • 响应流式 RPC:客户端向服务器发送请求,服务器返回一个响应消息流。你通过在响应类型前添加 stream 关键字来指定响应流式方法。

    // 获取给定矩形区域内的特征。结果将作为流而不是一次性返回(例如,响应消息中使用重复字段),因为矩形可能覆盖较大的区域并包含大量特征。
    rpc ListFeatures(Rectangle) returns (stream Feature) {}
  • 请求流式 RPC:客户端向服务器发送一系列消息。客户端完成发送消息后,等待服务器读取这些消息并返回响应。你通过在请求类型前添加 stream 关键字来指定请求流式方法。

    // 接收一个正在遍历的路由上的点流,返回一个路由总结。
    rpc RecordRoute(stream Point) returns (RouteSummary) {}
  • 双向流式 RPC:双方都向对方发送一系列消息。这两个流是独立操作的,因此客户端和服务器可以按任何顺序读取和写入消息:例如,服务器可以在接收所有客户端消息后再写入响应,或者可以交替读取消息然后写入响应,或其他任意组合。每个流中的消息顺序会被保留。通过在请求和响应类型前都添加 stream 关键字来指定这种类型的方法。

    // 接收在路由遍历过程中发送的路由笔记流,同时接收来自其他用户的其他路由笔记。
    rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

我们的 .proto 文件还包含所有请求和响应类型的协议缓冲消息类型定义——例如,这里是 Point 消息类型:

// 点表示为经纬度对,采用 E7 表示法(经度和纬度乘以 10^7 并四舍五入为整数)。
// 纬度应该在 +/- 90 度范围内,经度应该在 +/- 180 度范围内(包括)。
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

生成客户端代码

可以通过 gRPC PHP Protoc 插件生成 .proto 文件的 PHP 客户端存根实现。要编译插件:

make grpc_php_plugin

生成客户端存根实现的 .php 文件:

cd grpc
protoc --proto_path=examples/protos \
  --php_out=examples/php/route_guide \
  --grpc_out=examples/php/route_guide \
  --plugin=protoc-gen-grpc=bins/opt/grpc_php_plugin \
  ./examples/protos/route_guide.proto

或者如果你通过源代码构建了 grpc-php-plugin,可以在 grpc/example/php/route_guide 目录下运行帮助脚本:

./route_guide_proto_gen.sh

将在 examples/php/route_guide 目录下生成多个文件。你无需修改这些文件。

要加载这些生成的文件,请在 examples/php 目录下的 composer.json 文件中添加以下部分:

"autoload": {
  "psr-4": {
    "": "route_guide/"
  }
}

该文件包含:

  • 用于填充、序列化和检索我们的请求和响应消息类型的所有协议缓冲代码。

  • 一个名为 Routeguide\RouteGuideClient 的类,允许客户端调用 RouteGuide 服务中定义的方法。

创建客户端

在本节中,我们将介绍如何为 RouteGuide 服务创建一个 PHP 客户端。你可以在 examples/php/route_guide/route_guide_client.php 中查看完整的示例客户端代码。

构建客户端对象

要调用服务方法,我们首先需要创建一个客户端对象,即生成的 RouteGuideClient 类的实例。该类的构造函数接受我们要连接的服务器地址和端口:

$client = new Routeguide\RouteGuideClient('localhost:50051', [
    'credentials' => Grpc\ChannelCredentials::createInsecure(),
]);

调用服务方法

现在让我们看看如何调用我们的服务方法。

简单 RPC

调用简单的 RPC GetFeature 几乎和调用本地异步方法一样简单:

$point = new Routeguide\Point();
$point->setLatitude(409146138);
$point->setLongitude(-746188906);
list($feature, $status) = $client->GetFeature($point)->wait();

如你所见,我们创建并填充了一个请求对象,即 Routeguide\Point 对象。然后,我们在存根上调用该方法,传递请求对象。如果没有错误,我们可以从响应对象中读取服务器的响应信息,即 Routeguide\Feature 对象。

print sprintf("Found %s \n  at %f, %f\n", $feature->getName(),$feature->getLocation()->getLatitude() / COORD_FACTOR,
$feature->getLocation()->getLongitude() / COORD_FACTOR);

流式 RPC

接下来我们来看一下流式方法。以下是调用服务器端流式方法 ListFeatures 的示例,该方法返回一个地理特征的流:

$lo_point = new Routeguide\Point();
$hi_point = new Routeguide\Point();

$lo_point->setLatitude(400000000);
$lo_point->setLongitude(-750000000);
$hi_point->setLatitude(420000000);
$hi_point->setLongitude(-730000000);

$rectangle = new Routeguide\Rectangle();
$rectangle->setLo($lo_point);
$rectangle->setHi($hi_point);

$call = $client->ListFeatures($rectangle);
// 这是一个响应流迭代器
$features = $call->responses();
foreach ($features as $feature) {
  // 处理每个特征
} // 当服务器表示没有更多响应时,循环会结束。

$call→responses() 方法调用返回一个迭代器。当服务器发送响应时,每次都会返回一个 $feature 对象,直到服务器表示没有更多响应时,循环才会结束。

客户端流式方法 RecordRoute 类似,不过我们需要为每个要写入的点调用 $call→write($point),并最终获取一个 Routeguide\RouteSummary

$call = $client->RecordRoute();

for ($i = 0; $i < $num_points; $i++) {
  $point = new Routeguide\Point();
  $point->setLatitude($lat);
  $point->setLongitude($long);
  $call->write($point);
}

list($route_summary, $status) = $call->wait();

最后,我们来看一下双向流式 RPC RouteChat()。在这种情况下,我们只需传递一个上下文到该方法,便可以获得一个 BidiStreamingCall 流对象,我们可以用它来同时进行写入和读取消息。

$call = $client->RouteChat();

从客户端写入消息:

foreach ($notes as $n) {
  $point = new Routeguide\Point();
  $point->setLatitude($lat = $n[0]);
  $point->setLongitude($long = $n[1]);

  $route_note = new Routeguide\RouteNote();
  $route_note->setLocation($point);
  $route_note->setMessage($message = $n[2]);
  $call->write($route_note);
}
$call->writesDone();

从服务器读取消息:

while ($route_note_reply = $call->read()) {
  // 处理 $route_note_reply
}

每一方都会按照写入的顺序收到对方的消息,客户端和服务器可以按任意顺序读写——流是完全独立操作的。