基础教程

Objective-C 中 gRPC 的基础教程介绍。

基础教程

Objective-C 中 gRPC 的基础教程介绍。

本教程为 Objective-C 程序员提供 gRPC 的基本入门介绍。

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

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

它假定你熟悉协议缓冲区。请注意,本教程中的示例使用了协议缓冲区语言的 proto3 版本:你可以在proto3 语言指南Objective-C 生成的代码指南中找到更多信息。

为什么使用 gRPC?

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

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

示例代码和设置

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

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

然后将当前目录更改为 examples/objective-c/route_guide

cd examples/objective-c/route_guide

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

你还应该安装Cocoapods,以及生成客户端库代码的相关工具(以及另一种语言的服务器,用于测试)。你可以按照这些设置说明获取后者。

试一试!

要尝试示例应用程序,我们需要一个在本地运行的 gRPC 服务器。例如,让我们编译并运行此存储库中的 C++ 服务器

pushd ../../cpp/route_guide
make
./route_guide_server &
popd

现在让 Cocoapods 为我们的 .proto 文件生成并安装客户端库

pod install

(这可能必须编译 OpenSSL,如果 Cocoapods 还没有在你计算机的缓存中,则需要大约 15 分钟)。

最后,打开由 Cocoapods 创建的 XCode 工作区,并运行该应用程序。你可以在 ViewControllers.m 中检查调用代码,并在 XCode 的日志控制台中查看结果。

接下来的章节将逐步指导你了解如何定义此 proto 服务、如何从中生成客户端库以及如何创建使用该库的应用程序。

定义服务

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

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

service RouteGuide {
   ...
}

然后在你的服务定义中定义 rpc 方法,指定它们的请求和响应类型。协议缓冲区允许你定义四种服务方法,所有这些方法都在 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;
}

您可以通过在文件顶部添加 objc_class_prefix 选项来指定用于生成的类的前缀。例如

option objc_class_prefix = "RTG";

生成客户端代码

接下来,我们需要从我们的 .proto 服务定义中生成 gRPC 客户端接口。我们使用协议缓冲区编译器 (protoc) 和一个特殊的 gRPC Objective-C 插件来完成此操作。

为了简单起见,我们提供了一个 Podspec 文件,它使用适当的插件、输入和输出来为您运行 protoc,并描述如何编译生成的文件。您只需要在此目录 (examples/objective-c/route_guide) 中运行即可

pod install

在将生成的库安装到此示例的 XCode 项目之前,该文件会运行

protoc -I ../../protos --objc_out=Pods/RouteGuide --objcgrpc_out=Pods/RouteGuide ../../protos/route_guide.proto

运行此命令会在 Pods/RouteGuide/ 下生成以下文件

  • RouteGuide.pbobjc.h,声明您的生成的消息类的头文件。
  • RouteGuide.pbobjc.m,其中包含您的消息类的实现。
  • RouteGuide.pbrpc.h,声明您的生成的服务类的头文件。
  • RouteGuide.pbrpc.m,其中包含您的服务类的实现。

这些文件包含

  • 所有用于填充、序列化和检索我们的请求和响应消息类型的协议缓冲区代码。
  • 一个名为 RTGRouteGuide 的类,它允许客户端调用 RouteGuide 服务中定义的方法。

您还可以使用提供的 Podspec 文件从任何其他 proto 服务定义生成客户端代码;只需替换名称(与文件名匹配)、版本和其他元数据即可。

创建客户端应用程序

在本节中,我们将了解如何为我们的 RouteGuide 服务创建一个 Objective-C 客户端。您可以在 examples/objective-c/route_guide/ViewControllers.m 中看到我们完整的示例客户端代码。

构建服务对象

要调用服务方法,我们首先需要创建一个服务对象,即生成的 RTGRouteGuide 类的实例。该类的指定初始化器需要一个 NSString *,其中包含我们要连接的服务器地址和端口

#import <GRPCClient/GRPCCall+Tests.h>
#import <RouteGuide/RouteGuide.pbrpc.h>
#import <GRPCClient/GRPCTransport.h>

static NSString * const kHostAddress = @"localhost:50051";
...
GRPCMutableCallOptions *options = [[GRPCMutableCallOptions alloc] init];
options.transport = GRPCDefaultTransportImplList.core_insecure;

RTGRouteGuide *service = [[RTGRouteGuide alloc] initWithHost:kHostAddress callOptions:options];

请注意,我们的服务是使用不安全的传输构建的。这是因为我们将用于测试客户端的服务器不使用 TLS。这没关系,因为它将在我们的开发机器上本地运行。但是,最常见的情况是连接到互联网上的 gRPC 服务器,该服务器通过 TLS 运行 gRPC。对于这种情况,不需要设置选项 options.transport,因为 gRPC 默认会使用安全的 TLS 传输。

调用服务方法

现在让我们看看如何调用我们的服务方法。您将看到,所有这些方法都是异步的,因此您可以从应用程序的主线程中调用它们,而无需担心冻结您的 UI 或 OS 终止您的应用程序。

简单 RPC

调用简单的 RPC GetFeature 与调用 Cocoa 上的任何其他异步方法一样简单。


RTGPoint *point = [RTGPoint message];
point.latitude = 40E7;
point.longitude = -74E7;

GRPCUnaryResponseHandler *handler =
    [[GRPCUnaryResponseHandler alloc] initWithResponseHandler:
        ^(RTGFeature *response, NSError *error) {
          if (response) {
            // Successful response received
          } else {
            // RPC error
          }
        }
                                        responseDispatchQueue:nil];

[[service getFeatureWithMessage:point responseHandler:handler callOptions:nil] start];

如您所见,我们创建并填充一个请求协议缓冲区对象(在我们的例子中是 RTGPoint)。然后,我们在服务对象上调用该方法,将请求传递给它,并传递一个块来处理响应(或任何 RPC 错误)。如果 RPC 成功完成,则会使用 nil 错误参数调用处理程序块,我们可以从响应参数中读取服务器的响应信息。相反,如果发生某些 RPC 错误,则会使用 nil 响应参数调用处理程序块,我们可以从错误参数中读取问题的详细信息。

流式 RPC

现在让我们看看我们的流式方法。这是我们调用响应流式方法 ListFeatures 的地方,它会导致我们的客户端应用程序接收到一系列地理 RTGFeature


- (void)didReceiveProtoMessage(GPBMessage *)message {
  if (message) {
    NSLog(@"Found feature at %@ called %@.", response.location, response.name);
  }
}

- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error {
  if (error) {
    NSLog(@"RPC error: %@", error);
  }
}

- (void)execRequest {
  ...
  [[service listFeaturesWithMessage:rectangle responseHandler:self callOptions:nil] start];
}

请注意,视图控制器对象本身处理响应,而不是提供响应处理程序对象。当收到消息时,会调用方法 didReceiveProtoMessage:;它可以被调用任意次数。当调用完成并且从服务器接收到 gRPC 状态时(或当调用期间发生任何错误时),会调用方法 didCloseWithTrailingMetadata:

请求流式方法 RecordRoute 期望从客户端接收 RTGPoint 流。在调用开始后,可以将此流写入 gRPC 调用对象。

RTGPoint *point1 = [RTGPoint message];
point.latitude = 40E7;
point.longitude = -74E7;

RTGPoint *point2 = [RTGPoint message];
point.latitude = 40E7;
point.longitude = -74E7;

GRPCUnaryResponseHandler *handler =
    [[GRPCUnaryResponseHandler alloc] initWithResponseHandler:
        ^(RTGRouteSummary *response, NSError *error) {
            if (response) {
              NSLog(@"Finished trip with %i points", response.pointCount);
              NSLog(@"Passed %i features", response.featureCount);
              NSLog(@"Travelled %i meters", response.distance);
              NSLog(@"It took %i seconds", response.elapsedTime);
            } else {
              NSLog(@"RPC error: %@", error);
            }
        }
                                        responseDispatchQueue:nil];
GRPCStreamingProtoCall *call =
    [service recordRouteWithResponseHandler:handler callOptions:nil];
[call start];
[call writeMessage:point1];
[call writeMessage:point2];
[call finish];

请注意,由于 gRPC 调用对象不知道请求流的结尾,因此当请求流完成时,用户必须调用 finish: 方法。

最后,让我们看看我们的双向流式 RPC RouteChat()。调用双向流式 RPC 的方式只是如何调用请求流式 RPC 和响应流式 RPC 的组合。


- (void)didReceiveProtoMessage(GPBMessage *)message {
  RTGRouteNote *note = (RTGRouteNote *)message;
  if (note) {
    NSLog(@"Got message %@ at %@", note.message, note.location);
  }
}

- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error {
  if (error) {
    NSLog(@"RPC error: %@", error);
  } else {
    NSLog(@"Chat ended.");
  }
}

- (void)execRequest {
  ...
  GRPCStreamingProtoCall *call =
      [service routeChatWithResponseHandler:self callOptions:nil];
  [call start];
  [call writeMessage:note1];
  ...
  [call writeMessage:noteN];
  [call finish];
}