基础教程

Ruby 中 gRPC 的基本教程介绍。

基础教程

Ruby 中 gRPC 的基本教程介绍。

本教程为 Ruby 程序员提供了使用 gRPC 的基本入门介绍。

通过学习本示例,您将了解如何

  • 在 .proto 文件中定义服务。
  • 使用协议缓冲区编译器生成服务器和客户端代码。
  • 使用 Ruby gRPC API 为您的服务编写一个简单的客户端和服务器。

它假设您已阅读gRPC 简介并熟悉协议缓冲区。请注意,本教程中的示例使用协议缓冲区语言的 proto3 版本:您可以在proto3 语言指南中找到更多信息。

为什么要使用 gRPC?

我们的示例是一个简单的路线映射应用程序,允许客户端获取有关其路线上要素的信息、创建其路线的摘要以及与服务器和其他客户端交换路线信息(例如交通更新)。

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

示例代码和设置

我们的教程示例代码位于 grpc/grpc/examples/ruby/route_guide。要下载示例,请运行以下命令克隆 grpc 存储库

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

然后将当前目录更改为 examples/ruby/route_guide

cd examples/ruby/route_guide

您还应该安装相关的工具来生成服务器和客户端接口代码 - 如果您还没有,请按照快速入门中的设置说明进行操作。

定义服务

我们的第一步(正如您从gRPC 简介中所知)是使用协议缓冲区定义 gRPC 服务 以及方法 请求响应 类型。您可以在examples/protos/route_guide.proto中查看完整的 .proto 文件。

要定义服务,您需要在 .proto 文件中指定一个命名的 service

service RouteGuide {
   ...
}

然后在您的服务定义中定义 rpc 方法,指定它们的请求和响应类型。gRPC 允许您定义四种服务方法,所有这些方法都在 RouteGuide 服务中使用

  • 一个简单 RPC,其中客户端使用存根向服务器发送请求并等待响应返回,就像一个正常的函数调用一样。

    // Obtains the feature at a given position.
    rpc GetFeature(Point) returns (Feature) {}
    
  • 一个服务器端流式 RPC,其中客户端向服务器发送请求并获取流以读取一系列消息。客户端从返回的流中读取,直到没有更多消息。正如您在我们的示例中看到的那样,您可以通过在响应类型之前放置 stream 关键字来指定服务器端流式方法。

    // Obtains the Features available within the given Rectangle.  Results are
    // streamed rather than returned at once (e.g. in a response message with a
    // repeated field), as the rectangle may cover a large area and contain a
    // huge number of features.
    rpc ListFeatures(Rectangle) returns (stream Feature) {}
    
  • 一个客户端流式 RPC,其中客户端写入一系列消息并将它们发送到服务器,再次使用提供的流。一旦客户端完成写入消息,它会等待服务器读取所有消息并返回其响应。您可以通过在请求类型之前放置 stream 关键字来指定客户端流式方法。

    // Accepts a stream of Points on a route being traversed, returning a
    // RouteSummary when traversal is completed.
    rpc RecordRoute(stream Point) returns (RouteSummary) {}
    
  • 一个双向流式 RPC,其中双方都使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照他们喜欢的任何顺序读取和写入:例如,服务器可以等待接收所有客户端消息,然后写入其响应,或者它可以交替读取消息然后写入消息,或者其他读取和写入的组合。每个流中消息的顺序都会被保留。您可以通过在请求和响应之前都放置 stream 关键字来指定这种类型的方法。

    // Accepts a stream of RouteNotes sent while a route is being traversed,
    // while receiving other RouteNotes (e.g. from other users).
    rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
    

我们的 .proto 文件还包含我们服务方法中使用的所有请求和响应类型的协议缓冲区消息类型定义 - 例如,这是 Point 消息类型

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

生成客户端和服务器代码

接下来,我们需要从我们的 .proto 服务定义中生成 gRPC 客户端和服务端接口。我们使用协议缓冲区编译器 protoc 和一个特殊的 gRPC Ruby 插件来实现这一点。

如果您想自己运行此操作,请确保您已安装 gRPCprotoc

完成上述操作后,可以使用以下命令生成 Ruby 代码。

grpc_tools_ruby_protoc -I ../../protos --ruby_out=../lib --grpc_out=../lib ../../protos/route_guide.proto

运行此命令会在 lib 目录中重新生成以下文件

  • lib/route_guide.pb 定义了一个模块 Examples::RouteGuide
    • 其中包含所有协议缓冲区代码,用于填充、序列化和检索我们的请求和响应消息类型
  • lib/route_guide_services.pb,使用 stub 和 service 类扩展了 Examples::RouteGuide
    • 一个 Service 类,在定义 RouteGuide 服务实现时用作基类
    • 一个 Stub 类,可用于访问远程 RouteGuide 实例

创建服务器

首先,让我们看看如何创建 RouteGuide 服务器。如果您只对创建 gRPC 客户端感兴趣,则可以跳过此部分,直接转到 创建客户端(不过您可能会觉得它也很有趣!)。

要使我们的 RouteGuide 服务正常工作,需要完成两部分工作

  • 实现从我们的服务定义生成的服务接口:执行我们服务的实际“工作”。
  • 运行一个 gRPC 服务器来监听来自客户端的请求并返回服务响应。

您可以在 examples/ruby/route_guide/route_guide_server.rb 中找到我们的示例 RouteGuide 服务器。让我们仔细看看它的工作原理。

实现 RouteGuide

如您所见,我们的服务器有一个 ServerImpl 类,它扩展了生成的 RouteGuide::Service

# ServerImpl provides an implementation of the RouteGuide service.
class ServerImpl < RouteGuide::Service

ServerImpl 实现了我们所有的服务方法。让我们先看看最简单的类型 GetFeature,它只是从客户端获取一个 Point,并从其数据库中返回相应的特征信息到一个 Feature 中。

def get_feature(point, _call)
  name = @feature_db[{
    'longitude' => point.longitude,
    'latitude' => point.latitude }] || ''
  Feature.new(location: point, name: name)
end

该方法被传递一个 RPC 的 _call,客户端的 Point 协议缓冲区请求,并返回一个 Feature 协议缓冲区。在该方法中,我们使用适当的信息创建 Feature,然后 return 它。

现在让我们看看更复杂的东西 - 流式 RPC。ListFeatures 是一个服务器端流式 RPC,因此我们需要向客户端发送多个 Feature

# in ServerImpl

  def list_features(rectangle, _call)
    RectangleEnum.new(@feature_db, rectangle).each
  end

如您所见,这里的请求对象是一个 Rectangle,我们的客户端希望在其中找到 Feature,但我们不是返回一个简单的响应,而是需要返回一个 Enumerator 来产生响应。在该方法中,我们使用辅助类 RectangleEnum 来充当 Enumerator 实现。

类似地,客户端流式方法 record_route 使用一个 Enumerable,但这里它从 call 对象获得,我们在之前的示例中忽略了它。call.each_remote_read 依次产生客户端发送的每条消息。

call.each_remote_read do |point|
  ...
end

最后,让我们看看我们的双向流式 RPC route_chat

def route_chat(notes)
  RouteChatEnumerator.new(notes, @received_notes).each_item
end

这里的方法接收一个 Enumerable,但也返回一个 Enumerator 来产生响应。尽管每一方都将始终按写入顺序接收对方的消息,但客户端和服务端都可以按任何顺序读取和写入 — 流是完全独立运行的。

启动服务器

在我们实现所有方法之后,我们还需要启动一个 gRPC 服务器,以便客户端可以实际使用我们的服务。以下代码片段显示了我们如何为我们的 RouteGuide 服务执行此操作

port = '0.0.0.0:50051'
s = GRPC::RpcServer.new
s.add_http2_port(port, :this_port_is_insecure)
GRPC.logger.info("... running insecurely on #{port}")
s.handle(ServerImpl.new(feature_db))
# Runs the server with SIGHUP, SIGINT and SIGQUIT signal handlers to
#   gracefully shutdown.
# User could also choose to run server via call to run_till_terminated
s.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])

如您所见,我们使用 GRPC::RpcServer 构建并启动我们的服务器。为此,我们

  1. 创建我们的服务实现类 ServerImpl 的实例。
  2. 使用构建器的 add_http2_port 方法指定我们希望用于监听客户端请求的地址和端口。
  3. 使用 GRPC::RpcServer 注册我们的服务实现。
  4. GRPC::RpcServer 上调用 run,为我们的服务创建并启动一个 RPC 服务器。

创建客户端

在本节中,我们将研究如何为我们的 RouteGuide 服务创建一个 Ruby 客户端。您可以在 examples/ruby/route_guide/route_guide_client.rb 中查看我们完整的示例客户端代码。

创建存根

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

我们使用从我们的 .proto 生成的 RouteGuide 模块的 Stub 类。

stub = RouteGuide::Stub.new('localhost:50051')

调用服务方法

现在让我们看看如何调用我们的服务方法。请注意,gRPC Ruby 仅提供每个方法的阻塞/同步版本:这意味着 RPC 调用会等待服务器响应,并且将返回响应或引发异常。

简单 RPC

调用简单的 RPC GetFeature 几乎与调用本地方法一样简单。

GET_FEATURE_POINTS = [
  Point.new(latitude:  409_146_138, longitude: -746_188_906),
  Point.new(latitude:  0, longitude: 0)
]
..
  GET_FEATURE_POINTS.each do |pt|
    resp = stub.get_feature(pt)
	...
    p "- found '#{resp.name}' at #{pt.inspect}"
  end

如您所见,我们创建并填充一个请求协议缓冲区对象(在我们的例子中是 Point),并创建一个供服务器填充的响应协议缓冲区对象。最后,我们调用 stub 上的方法,并将上下文、请求和响应传递给它。如果该方法返回 OK,那么我们可以从响应对象中读取来自服务器的响应信息。

流式 RPC

现在让我们看看我们的流式方法。如果您已经阅读了 创建服务器,那么其中一些内容可能会非常熟悉 - 流式 RPC 在两端的实现方式类似。这是我们调用服务器端流式方法 list_features 的地方,该方法返回一个 FeaturesEnumerable

resps = stub.list_features(LIST_FEATURES_RECT)
resps.each do |r|
  p "- found '#{r.name}' at #{r.location.inspect}"
end

可以使用多个线程和 return_op: true 标志来实现 RPC 流的非阻塞使用。当传递 return_op: true 标志时,RPC 的执行被延迟,并返回一个 Operation 对象。然后可以通过调用操作 execute 函数在另一个线程中执行 RPC。主线程可以利用上下文方法和 getter(例如 statuscancelled?cancel)来管理 RPC。这对于会阻塞主线程不可接受的时间段的持久或长时间运行的 RPC 会话非常有用。

op = stub.list_features(LIST_FEATURES_RECT, return_op: true)
Thread.new do 
  resps = op.execute
  resps.each do |r|
    p "- found '#{r.name}' at #{r.location.inspect}"
  end
rescue GRPC::Cancelled => e
  p "operation cancel called - #{e}"
end

# controls for the operation
op.status
op.cancelled?
op.cancel # attempts to cancel the RPC with a GRPC::Cancelled status; there's a fundamental race condition where cancelling the RPC can race against RPC termination for a different reason - invoking `cancel` doesn't necessarily guarantee a `Cancelled` status

客户端流式方法 record_route 类似,只是我们向服务器传递一个 Enumerable

...
reqs = RandomRoute.new(features, points_on_route)
resp = stub.record_route(reqs.each)
...

最后,让我们看看我们的双向流式 RPC route_chat。在这种情况下,我们将 Enumerable 传递给该方法,并返回一个 Enumerable

sleeping_enumerator = SleepingEnumerator.new(ROUTE_CHAT_NOTES, 1)
stub.route_chat(sleeping_enumerator.each_item) { |r| p "received #{r.inspect}" }

尽管此示例没有很好地显示,但每个 enumerable 都是相互独立的 - 客户端和服务端都可以按任何顺序读取和写入 — 流是完全独立运行的。

试试看!

从示例目录工作

cd examples/ruby

构建客户端和服务器

gem install bundler && bundle install

运行服务器

bundle exec route_guide/route_guide_server.rb ../python/route_guide/route_guide_db.json

从不同的终端,运行客户端

bundle exec route_guide/route_guide_client.rb ../python/route_guide/route_guide_db.json
上次修改时间:2024 年 11 月 25 日:feat: move the $ shell line indicator to scss (#1354) (ab8b3af)