将 HTTP/JSON 转码为 gRPC

Cloud Endpoints 支持协议转码,以便客户端可以使用 HTTP/JSON 访问您的 gRPC API。可扩展服务代理 (ESP) 可将 HTTP/JSON 转码为 gRPC。

本指南介绍了以下内容:

  • 如何使用 .proto 文件中的注释来指定从 HTTP/JSON 到 gRPC 的数据转换
  • 如何在 Endpoints 中部署您的服务以使用此功能
  • 在哪里可以找到有关为 gRPC 服务设计和实现转码的更多参考信息

假设您已完成我们的 gRPC 教程并熟悉适用于 gRPC API 的 Endpoints 基本概念。

设计易于转码的 API

转码涉及将 HTTP/JSON 请求及其参数映射到 gRPC 方法及其参数和返回类型。因此,尽管可以将 HTTP/JSON 请求映射到任意 API 方法,但如果您以面向资源的方式设计 gRPC API 的结构(就像传统 HTTP REST API 一样),则有助于实现映射。换句话说,您可设计 API 服务,让其使用少量标准方法,并与操作该服务的资源和资源集合(本身是一种资源类型)的 GETPUTHTTP 谓词相对应。这些标准方法包括 ListGetCreateUpdateDelete

如有必要,API 还可以采用一些非标准的自定义方法,不过这些方法的映射并不简单。

您可以在 API 设计指南中更详细地了解面向资源的设计和标准转码映射。该设计指南是 Google 在设计 Cloud API 等公共 API 时遵循的设计标准。虽然您无需遵循该指南来使用 gRPC 转码,但我们强烈建议您使用该转码。具体来说,以下页面将帮助您了解这些设计原则,并在您的方法中添加有用的转码映射:

以下参考页面可能也有用:

在本文档的其余部分,您会用到在我们的教程中使用的 Bookstore 示例,该示例已采用上述原则。Bookstore 具有“图书”资源的“书架”集合,用户可以执行 ListGetCreateDelete 方法。

何处配置转码

gRPC 转码默认启用,您可在不进行任何配置的情况下使用它。按照相关说明来部署使用转码的服务。之后,当您以 JSON 格式在 HTTP 请求正文中通过方法的请求消息字段值(如有)向网址路径 GRPC_SERVICE_FULL_NAME/METHOD_NAME> 发送 HTTP POST 请求时,ESP 会将请求消息发送给相应的 gRPC 方法。在上述示例中,GRPC_SERVICE_FULL_NAME 是 gRPC 服务的全名,METHOD_NAME 是该方法的名称。

例如,如果您向 Bookstore 的 ListShelves 网址发送 POST(如下所示):

curl -XPOST http://mydomain/endpoints.examples.bookstore.Bookstore/ListShelves

则您会获得 JSON 形式的最新书架列表。

不过,在 HTTP 接口设计方面,强烈建议您按照本文档其余部分中的说明,明确配置映射。

通过针对服务配置的 gRPC API 配置标准,您可以准确指定数据应如何从 HTTP/JSON 转换为 gRPC。为此,您可以使用两种机制:使用 .proto 文件中的直接注释,以及在属于 gRPC API 配置文件的 YAML 中配置。建议您使用 proto 注释,这样便于读取和维护。如需详细了解 YAML 配置以及何时可能需要用到它,请参阅在 YAML 中配置转码

下面是一个使用 Bookstore 建议的方法的示例:

// Returns a specific bookstore shelf.
rpc GetShelf(GetShelfRequest) returns (Shelf) {
  // Client example - returns the first shelf:
  //   curl http://DOMAIN_NAME/v1/shelves/1
  option (google.api.http) = { get: "/v1/shelves/{shelf}" };
}

...
// Request message for GetShelf method.
message GetShelfRequest {
  // The ID of the shelf resource to retrieve.
  int64 shelf = 1;
}

注解告知 ESP 使用网址 http://mydomain/v1/shelves/1 发出 HTTP GET 请求会调用 gRPC 服务器的 GetShelf() 方法,使用包含所请求的书架 ID shelf(在此示例中为 1)的 GetShelfRequest

添加转码映射

本部分介绍 Bookstore 示例中的其他一些映射注释。Bookstore 示例中有两个示例 proto 文件,以便您无论是否使用转码映射都可部署它,还可比较 proto 文件中的差异:

有关指定转码映射的更全面的指导,请参阅标准方法自定义方法HTTP 规则参考

映射 List 方法

List 方法及其响应类型是在 .proto 文件中定义的:

  // Returns a list of all shelves in the bookstore.
  rpc ListShelves(google.protobuf.Empty) returns (ListShelvesResponse) {
    // Define HTTP mapping.
    // Client example (Assuming your service is hosted at the given 'DOMAIN_NAME'):
    //   curl http://DOMAIN_NAME/v1/shelves
    option (google.api.http) = { get: "/v1/shelves" };
  }
...
message ListShelvesResponse {
  // Shelves in the bookstore.
  repeated Shelf shelves = 1;
}

以粗体显示的注解指定了该方法的 HTTP 映射。

  • option (google.api.http) 指定此方法是一个 gRPC HTTP 映射注解。
  • get 指定此方法映射到 HTTP GET 请求。
  • "/v1/shelves"GET 请求在调用该方法时使用的网址路径模板(附加到服务的网域)。网址路径也称为资源路径,因为它通常用于指定您要使用的“对象”或资源。在本例中是指我们的 Bookstore 的所有书架资源。

例如,如果客户端通过向网址 http://mydomain/v1/shelves 发送 GET 来调用此方法,则 ESP 会调用 gRPC 方法 ListShelves()。然后,gRPC 后端返回书架,ESP 会将该书架转换为 JSON 格式并返回给客户端。

映射 Get 方法

Bookstore 的 GetShelf 方法及其请求和响应类型是在 .proto 文件中定义的:

// Returns a specific bookstore shelf.
rpc GetShelf(GetShelfRequest) returns (Shelf) {
  // Client example - returns the first shelf:
  //   curl http://DOMAIN_NAME/v1/shelves/1
  option (google.api.http) = { get: "/v1/shelves/{shelf}" };
}

...
// Request message for GetShelf method.
message GetShelfRequest {
  // The ID of the shelf resource to retrieve.
  int64 shelf = 1;
}
...
// A shelf resource.
message Shelf {
  // A unique shelf id.
  int64 id = 1;
  // A theme of the shelf (fiction, poetry, etc).
  string theme = 2;
}

以粗体显示的注解指定了该方法的 HTTP 映射。

  • option (google.api.http) 指定此方法是一个 gRPC HTTP 映射注解。
  • get 指定此方法映射到 HTTP GET 请求。
  • 如前所述,"/v1/shelves/{shelf}" 是请求的网址路径,但它先指定 /v1/shelves/,然后指定 {shelf}。该花括号表示法告知 ESP,{shelf} 中的任何内容都是 ESP 应在此方法的 GetShelfRequest 参数中为 shelf 提供的值。

如果客户端通过向网址 http://mydomain/v1/shelves/4 发送 GET 来调用此方法,则 ESP 会创建一个 shelf 值为 4GetShelfRequest,然后通过该请求调用 gRPC 方法 GetShelf()。然后,gRPC 后端返回所请求的 ID 为 4Shelf,ESP 再将其转换为 JSON 格式并返回给客户端。

此方法只需要客户端 shelf 提供一个请求字段值,该值是您在采用花括号“捕获型”表示法的网址路径模板中指定的。如有必要,您还可以捕获网址的多个部分,以识别请求的资源。例如,GetBook 方法需要客户端在网址中同时指定书架 ID 和图书 ID:

// Returns a specific book.
rpc GetBook(GetBookRequest) returns (Book) {
  // Client example - get the first book from the second shelf:
  //   curl http://DOMAIN_NAME/v1/shelves/2/books/1
  option (google.api.http) = { get: "/v1/shelves/{shelf}/books/{book}" };
}
...
// Request message for GetBook method.
message GetBookRequest {
  // The ID of the shelf from which to retrieve a book.
  int64 shelf = 1;
  // The ID of the book to retrieve.
  int64 book = 2;
}

除了将文字和捕获型括号用于字段值之外,网址路径模板还可使用通配符指示应捕获该网址部分中的全部内容。上述示例中使用的 {shelf} 表示法实际上是 {shelf=*} 的快捷方式。如需详细了解路径模板的规则,请查看 HTTP 规则参考

对于该方法类型,未指定 HTTP 请求正文。有关映射 Get 方法(包括使用查询参数)的更多准则,可查看标准方法

映射 Create 方法

Bookstore 的 CreateShelf 方法映射到 HTTP POST

  // Creates a new shelf in the bookstore.
  rpc CreateShelf(CreateShelfRequest) returns (Shelf) {
    // Client example:
    //   curl -d '{"theme":"Music"}' http://DOMAIN_NAME/v1/shelves
    option (google.api.http) = {
      post: "/v1/shelves"
      body: "shelf"
    };
  }
...
// Request message for CreateShelf method.
message CreateShelfRequest {
  // The shelf resource to create.
  Shelf shelf = 1;
}
...
// A shelf resource.
message Shelf {
  // A unique shelf id.
  int64 id = 1;
  // A theme of the shelf (fiction, poetry, etc).
  string theme = 2;
}
  • option (google.api.http) 指定此方法是一个 gRPC HTTP 映射注解。
  • post 指定此方法映射到 HTTP POST 请求。
  • 如前所述,"/v1/shelves" 是请求的网址路径。
  • body: "shelf" 在 HTTP 请求正文中用来以 JSON 格式指定您要添加的资源。

例如,如果客户端按如下所示的方式调用该方法:

curl -d '{"theme":"Music"}' http://DOMAIN_NAME/v1/shelves

ESP 使用 JSON 正文为 CreateShelfRequest 创建一个主题为 "Music"Shelf 值,然后调用 gRPC CreateShelf() 方法。请注意,客户端不提供 Shelfid 值。Bookstore 的书架 ID 由相关服务在创建新书架时提供。您应在 API 文档中为服务用户提供此类信息。

在正文中使用通配符

在正文映射中,可以使用特殊名称 * 来指示不受路径模板约束的每个字段应该映射到请求正文。此方式支持以下备用的 CreateShelf 方法定义。

  // Creates a new shelf in the bookstore.
  rpc CreateShelf(CreateShelfRequest) returns (Shelf) {
    // Client example:
    //   curl -d '{"shelf_theme":"Music", "shelf_size": 20}' http://DOMAIN_NAME/v1/shelves/123
    option (google.api.http) = {
      post: "/v1/shelves/{shelf_id}"
      body: "*"
    };
  }
...
// Request message for CreateShelf method.
message CreateShelfRequest {
  // A unique shelf id.
  int64 shelf_id = 1;
  // A theme of the shelf (fiction, poetry, etc).
  string shelf_theme = 2;
  // The size of the shelf
  int64 shelf_size = 3;
}
  • option (google.api.http) 指定此方法是一个 gRPC HTTP 映射注解。
  • post 指定此方法映射到 HTTP POST 请求。
  • "/v1/shelves/{shelf_id}" 是该请求的网址路径。{shelf_id}中的内容就是 shelf_id 字段在CreateShelfRequest中的值。
  • body: "*" 在 HTTP 请求正文中用于指定此示例中除 shelf_id 之外的所有剩余请求字段,这些字段是 shelf_themeshelf_size。对于 JSON 正文中具有这两个名称的任何字段,其值都将在 CreateShelfRequest 的相应字段中使用。

例如,如果客户端按如下方式调用该方法:

curl -d '{"shelf_theme":"Music", "shelf_size": 20}' http://DOMAIN_NAME/v1/shelves/123

ESP 使用 JSON 正文和路径模板来创建 CreateShelfRequest{shelf_id: 123 shelf_theme: "Music" shelf_size: 20},然后使用该模板调用 gRPC CreateShelf() 方法。如需了解详情,请参阅 HttpRule

在 YAML 中配置转码

另一种方法是在 gRPC API 配置 YAML 文件中而不是在 .proto 文件中指定 HTTP 到 gRPC 的映射。如果您有一个用于多项服务的 proto API 定义,并且为每项服务指定了不同的映射,则您可能需要在 YAML 文件中配置转码。

YAML 文件的 http 部分中的 rules 用来指定如何将 HTTP/JSON 请求映射到 gRPC 方法:

http:
  rules:
  ...
  #
  # 'GetShelf' is available via the GET HTTP verb and '/shelves/{shelf}' URL
  # path, where {shelf} is the value of the 'shelf' field of 'GetShelfRequest'
  # protobuf message.
  #
  # Client example - returns the first shelf:
  #   curl http://DOMAIN_NAME/v1/shelves/1
  #
  - selector: endpoints.examples.bookstore.Bookstore.GetShelf
    get: /v1/shelves/{shelf}
  ...

api_config_http.yaml 中提供了一个将此方法用于示例 Bookstore 服务的更完整示例。

部署使用转码的服务

部署使用转码的 gRPC 服务与部署任何其他 gRPC 服务大致相同,但有一项主要区别。在教程中,示例需要接受来自示例客户端的 gRPC 请求。但是,如果您要让 Bookstore 也接受 HTTP 请求,则需要为 ESP 执行一些额外配置。客户端使用 HTTP1.1 协议向 ESP 发送 JSON/HTTP 请求,因此 ESP 需要配置为使用 SSL(SSL 端口可支持这两种请求类型),或者需要启用一个特殊端口来接受这些调用。除了此项区别,部署方法与您所选环境对应的教程中介绍的部署方法大体相同。

确保已部署 HTTP 规则

如果您已下载教程的 Bookstore 示例,请注意您需要下载一个略有不同、带注释的 .proto 文件版本,http_bookstore.proto。您还需要在运行 protoc 之前从 GitHub 克隆 googleapis 代码库,因为您需要 annotations.proto 文件在包含路径中。

    git clone https://github.com/googleapis/googleapis

    GOOGLEAPIS_DIR=<your-local-googleapis-folder>

然后,在将配置部署到 Endpoints 时,通过 http_bookstore.proto 新建一个 .pb 描述符。

    protoc \
        --include_imports \
        --include_source_info \
        --proto_path=${GOOGLEAPIS_DIR} \
        --proto_path=. \
        --descriptor_set_out=api_descriptor.pb \
        http_bookstore.proto

如果您使用替代方法,即在 gRPC API 配置 YAML 文件中配置 HTTP 映射,则还需要确保在将配置部署到 Endpoints 时已部署相关规则。若要在 Bookstore 服务中尝试此方法,api_config.yaml 文件定义了其基本规则,api_config_http.yaml 文件定义了其 HTTP 规则:

    gcloud endpoints services deploy api_descriptor.pb api_config.yaml api_config_http.yaml

使用 SSL

如果为客户端与 ESP 之间的通信启用了 SSL,则客户端可以使用同一端口进行 gRPC 或 HTTP1.1 调用。您可以在启用 SSL 中了解如何为 Endpoints 服务设置 SSL。

在 Google Kubernetes Engine 配置文件 (GKE) 或 docker run 命令 (Compute Engine/Docker) 中使用 --ssl_port 标志来为 ESP 指定一个端口,用于接受 SSL 调用。

    args: [
      "--http_port", "8080",
      "--ssl_port", "443",  # enable SSL port at 443 to serve https requests
      "--backend",  "grpc://127.0.0.1:8081",  # gRPC backend.
      "--service", "SERVICE_NAME",
      "--rollout_strategy", "managed",
    ]

设置 HTTP1.1 端口

如果您不使用 SSL,则需要为 HTTP1.1 请求设置一个单独的端口,这是因为在不使用 SSL 的情况下 gRPC 和 HTTP1.1 不能共用同一端口。在 GKE 配置文件或 docker run 命令中使用 --http_port 标志来指定一个端口,用于接受 HTTP1.1 调用。如果您还希望 ESP 接受 gRPC 调用,则还需要使用 --http2_port 标志来指定 gRPC 端口。

    args: [
      "--http_port", "8080",  # for HTTP 1.1
      "--http2_port", "8090",  # for gRPC
      "--backend", "grpc://127.0.0.1:8081",  # gRPC backend.
      "--service", "SERVICE_NAME",
      "--rollout_strategy", "managed",
    ]

调用使用转码的服务

本部分介绍了服务设置以及如何对服务进行 HTTP 调用。

服务设置

假设您已经完成了适用于您所选环境的基本 gRPC 服务教程,并且有一个 GKE 集群或 Compute Engine 实例来运行示例。

  1. 首先确保您已将启用 HTTP 的 Bookstore 服务配置部署到 Endpoints,如确保已部署 HTTP 规则中所述。
  2. 按照您所选平台对应的教程中所述,部署后端和 ESP,并使用 --http_port 标志为 HTTP1.1 请求启用一个端口:

对服务进行 HTTP 调用

  1. 获取 ESP 的外部 IP 地址并将其设置为 $ESP_IP
  2. 通过 curl 发出以下 HTTP 请求

    curl http://$ESP_IP/v1/shelves
    

    (如果您在使用 SSL,请使用带有 https:// 的相同网址。)服务器会做出如下响应:

    {"shelves":[{"id":"1","theme":"Fiction"},{"id":"2","theme":"Fantasy"}]}
    

    如果输出显示二进制响应,请检查端口配置,因为您正在使用的可能是 HTTP2 端口,而不是 HTTP 端口。

  3. 尝试使用 Create 方法。CreateShelf 需要 API 密钥,因此您需要为项目创建一个密钥并将其设置为 $KEY。现在,请调用:

    curl -d '{"theme":"Music"}' http://$ESP_IP/v1/shelves?key=$KEY
    

    如果您再次调用 GetShelves,应该会看到新书架。