1. 介绍

gRPC 是开源的远程过程调用(RPC)框架,可在任何环境运行。使用 gRPC 可以有效地连接数据中心内和跨数据中心的服务,gRPC 具有可插拔的负载均衡、追踪、健康检查和身份验证支持。它也适用于将设备、移动应用程序和浏览器连接到后端服务的分布式计算的最后一英里。

gRPC 具有如下特点:

gRPC 使用 Protocol Buffers 作为它的接口定义语言(Interface Definition Language,IDL),及底层的消息交换格式。

1.1. 概览

在 gRPC 中,客户端应用程序可以直接调用部署在不同机器上的服务端应用程序中的方法,就好像它是本地对象一样,使用 gRPC 可以更容易地创建分布式应用程序和服务。与许多 RPC 系统一样,gRPC 基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。在服务端侧,服务端实现接口,运行 gRPC 服务,处理客户端调用。在客户端侧,客户端拥有存根(Stub,在某些语言中称为客户端),它提供与服务端相同的方法。

landing-2.svg

gRPC 客户端和服务端可以在各种环境中运行和相互通信 - 从 Google 内部的服务器到你自己的桌面 - 并且可以使用 gRPC 支持的任何语言编写。因此,比如,你可以轻松地用 Java 创建 gRPC 服务端,使用 Go、Python 或 Ruby 创建客户端。此外,最新的 Google API 将包含 gRPC 版本的接口,使你轻松地将 Google 功能构建到你的应用程序中。

1.2. 使用 Protocol Buffer

默认,gRPC 使用 Protocol Buffers,Google 的成熟的用于序列化结构化数据的开源机制(尽管可以使用其它数据格式,比如 JSON)。下面将快速介绍如何使用它。

当使用 Protocol Buffer 时,第一步是在 proto 文件中定义你想要序列化的数据的结构:它是扩展名为 .proto 的普通文本文件。Protocol Buffer 数据被构造为消息(message),其中每条消息是一个小的逻辑信息记录,包含一系列被称为字段(field)的名称-值对。下面是一个简单的示例:

在指定数据结构后,可以使用 Protocol Buffer 编译器 protoc 从 proto 定义中生成所选语言的数据访问类。它们为每个字段提供简单的访问器,比如 name()set_name(),以及用于将整个结构序列化成原始字节流和从原始字节流解析整个结构的方法。因此,比如,如果你选择的语言是 C++,那么在上面的示例上运行编译器将生成名为 Person 的类。然后可以在应用程序中使用该类填充、序列化和检索 Person Protocol Buffer 消息。

在普通的 proto 文件中定义 gRPC 服务,RPC 方法参数和返回类型被指定为 Protocol Buffer 消息:

gRPC 使用带特殊的 gRPC 插件的 protoc 从 proto 文件生成代码:你将获得生成的 gRPC 客户端和服务端代码,以及用于填充、序列化和检索消息类型的常规 Protocol Buffer 代码。如果想要了解更多关于 Protocol Buffer 的信息,包括如何在所选语言中安装带 gRPC 插件的 protoc,请参阅 protocol buffers documentation

1.3. Protocol Buffer 版本

虽然开源用户使用 protocol buffers 已有一段时间,但是本站点的大多数示例使用 Protocol Buffer 版本 3(proto3),它拥有略微简化的语法、一些有用的新特性,以及支持更多语言。Proto3 目前可用于 Java、C++、Dart、Python、Objective-C、C#、lite-runtime(Android Java)、Ruby 和来自 Protocol Buffer Github 仓库的 JavaScript,以及来自 golang/protobuf 官方包的 Go 语言生成器,更多语言正在开发中。你可以在 Proto3 语言指南和每种语言的可用参考文档中找到更多信息。参考文档也包含 .proto 文件格式的正式规范。

一般来说,虽然你可以使用 proto2(当前默认的 Protocol Buffer 版本),但我们建议你将 proto3 与 gRPC 一起使用,因为它可以让你使用 gRPC 支持的全部语言,以及避免 proto2 客户端与 proto3 服务端通信的兼容性问题,反之亦然。


2. 核心概念

2.1. 概览

2.1.1. 服务定义

与许多 RPC 系统一样,gRPC 基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。默认情况下,gRPC 使用 Protocol Buffer 作为接口定义语言(IDL),来描述服务接口和负载消息的结构。如果需要,也可以使用其它替代品。

gRPC 支持定义四种服务方法:

2.1.2. 使用 API

.proto 文件中的服务定义开始,gRPC 提供生成客户端和服务端代码的 Protocol Buffer 编译器插件。gRPC 用户通常在客户端调用这些 API,并且在服务端实现相应的 API。

2.1.3. 同步 vs. 异步

同步 RPC 调用阻塞直到服务端的响应到达,这与 RPC 所期望的过程调用的抽象最接近。另一方面,网络本质上是异步的,在许多情况下,能够在不阻塞当前线程的情况下启动 RPC 非常有用。

大多数语言中的 gRPC 编程 API 都有同步和异步两种风格。你可以在每种语言的教程和参考文档中找到更多信息。

2.2. RPC 生命周期(RPC Life Cycle)

2.2.1. 一元 RPC(Unary RPC)

首先看最简单的 RPC 类型,客户端发送单个请求,取回单个响应。

  1. 当客户端调用存根(Stub)方法时,服务端将收到通知,客户端使用用于本次调用的元数据(metadata)、方法名称和指定的截止时间(deadline)调用 RPC。
  2. 然后,服务端可以直接发送回它自己的初始元数据(必须在任何响应之前发送),也可以等待客户端的请求消息。先发生哪个,是特定于应用程序的。
  3. 一旦服务端拥有客户端的请求消息,它就会执行创建和填充响应所需的所有工作。然后将响应(如果成功)连同状态详细信息(状态代码和可选的状态消息)和可选的尾随元数据一起返回到客户端。
  4. 如果响应状态是 OK,那么客户端获取响应,在客户端侧完成响应。
2.2.2. 服务端流 RPC

服务端流 RPC 与一元 RPC 类似,不同之处在于服务端返回消息流,以响应客户端的请求。在发送所有消息后,服务端的状态详情(状态码和可选的状态消息)和可选的尾随元数据被发送到客户端。这完成服务端侧的处理。客户端在拥有服务端的所有消息后完成处理。

2.2.3. 客户端流 RPC

客户端流 RPC 与一元 RPC 类似,不同之处在于客户端向服务端发送消息流,而不是单个消息。服务端响应单条消息(以及它的状态详细信息和可选的尾随元数据),通常(但不一定)在它接收到所有客户端消息之后。

2.2.4. 双向流 RPC

在双向流 RPC 中,调用由调用方法的客户端发起,服务端接收客户端的元数据、方法名和截止时间。服务端可以选择发送回它的初始元数据,或者等待客户端开始流消息。

客户端侧和服务端侧流处理是特定于语言的,由于这两个流是独立的,客户端和服务端可以以任意顺序读写消息。例如,服务端可以等待到收到客户端的所有消息后,再写它的消息,或者服务端和客户端可以玩“乒乓”游戏 - 服务端接收请求,然后发送回响应,然后客户端根据响应发送另一个请求,以此类推。

2.2.5. 截止时间/超时

gRPC 允许客户端指定在 RPC 以 DEADLINE_EXCEEDED 错误终止之前,它们愿意等待多久。在服务端,服务可以查询特定的 RPC 是否超时,或者还剩下多少时间来完成该 RPC。

指定截止日期或超时是特定于语言的:一些语言 API 根据超时(持续时间)工作,一些语言 API 根据截止时间(固定的时间点)工作,并且可能有也可能没有默认截止时间。

2.2.6. RPC 终止

在 gRPC 中,客户端和服务端都对调用的成功做出独立的本地决定,并且它们的结论可能不匹配。这意味着,比如,你可以有一个在服务端成功完成(“我已经发送所有响应!”),但在客户端失败(“响应在截止时间之后到达!”)的 RPC。服务端也可以在客户端发送所有请求之前决定完成。

2.2.7. 取消 RPC

客户端和服务端都可以随时取消 RPC,取消将立即终止 RPC,因此不会执行进一步的工作。

警告

在取消前发生的变更不会回滚。

2.2.8. 元数据

元数据是键-值对列表形式的关于特定 RPC 调用(比如身份验证详细信息)的信息,其中键是字符串,值通常是字符串,但也可以是二进制数据。

键不区分大小写,由 ASCII 字母、数字和特殊字符 -_ 和 . 组成,并且不能以 grpc- 开头(为 gRPC 本身保留)。二进制值的键以 -bin 结尾,而 ASCII 值的键则不是。

用户定义的元数据不会被 gRPC 使用,它允许客户端向服务端提供与本次调用相关的信息,反之亦然。

对元数据的访问依赖于语言。

2.2.9. 通道

gRPC 通道提供到指定主机和端口上的 gRPC 服务的连接。在创建客户端存根时使用。客户端可以指定通道参数来修改 gRPC 的默认行为,比如打开或关闭消息压缩。通道有状态,包括已连接和空闲。

gRPC 如何处理关闭通道取决于语言。有些语言也允许查询通道状态。


3. 快速入门 Go gRPC

3.1. 环境说明

3.2. 创建测试项目

3.3. 给 protocol 编译器安装 Go 插件

  1. 使用下述命令为 Go 安装 protocol 编译器插件:

  2. 更新 PATH,以便 protoc 编译器可以找到插件:

3.4. 安装依赖包

3.5. 项目结构

go.mod:

greeter_client/main.go:

greeter_server/main.go:

hellogrpc/hellogrpc.proto:

3.6. 运行

在项目根目录运行如下命令:

这将生成 hellogrpc.pb.gohellogrpc_grpc.pb.go 文件,它们包含:

3.6.1. 运行服务端
3.6.2. 从另外一个终端,运行客户端

将看到如下输出(时间已被省略):


4. 基础教程 Go gRPC

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

4.1. 为什么使用 gRPC?

使用 gRPC,我们可以在 .proto 文件中定义我们的服务一次,生成 gRPC 支持的任何语言的客户端和服务端,它们可以在从大型数据中心的服务器到你自己的平板电脑的各种环境中运行 - 所有不同语言和环境之间的通信复杂性由 gRPC 处理。同时也可以获得使用 Protocol Buffer 的所有优势,包括高效的序列化、简单的 IDL 和易于更新的接口。

4.2. 获取代码

示例代码是 grpc-go 仓库的一部分。

  1. 下载 zip 文件形式的仓库(Download the repo as a zip file),然后解压缩,或克隆仓库:

  2. 切换到示例目录:

4.3. 定义服务

第一步是使用 protocol buffers 定义 gRPC 服务和方法 request 和 response 类型。查看 routeguide/route_guide.proto 获取完整的 .proto 文件。

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

然后在服务定义里面定义 rpc 方法,指定它们的请求和响应类型。gRPC 支持定义四种类型的服务方法:

.proto 文件还包含我们的服务方法中使用的所有请求和响应类型的 Protocol Buffer 消息类型定义 - 比如,下面是 Point 消息类型:

4.4. 生成客户端和服务端代码

接下来我们需要从 .proto 服务定义生成 gRPC 客户端和服务端接口。使用带特殊的 gRPC Go 插件的 Protocol Buffer 编译器 protoc 完成这项工作。这与我们在快速入门中做的事情类似。

examples/route_guide 目录,运行如下命令:

运行该命令将在 routeguide 目录生成如下文件:

4.5. 创建服务端

要使我们的 RouteGuide 服务发挥作用,需要做两部分工作:

你可以在 server/server.go 找到 RouteGuide 示例服务。

4.5.1. 实现 RouteGuide

如你所见,我们的服务有一个实现生成的 routeGuideServer 接口的 routeGuideServer 结构体类型:

4.5.2. 简单 RPC

routeGuideServer 实现所有服务方法。我们首先看最简单的类型,GetFeature,它仅从客户端获取一个 Point,然后从数据库中返回 Feature 里的相应特性信息。

该方法接收用于 RPC 的上下文对象和客户端的 Point Protocol Buffer 请求。它返回带有响应信息的 Feature Protocol Buffer 对象,以及一个 error 对象。在该方法中,我们使用相应信息填充 Feature,然后连同 nil 错误一起返回它,告诉 gRPC 我们已经完成 RPC 处理,该 Feature 可被返回给客户端。

4.5.3. 服务端流 RPC

ListFeatures 是一个服务端流 RPC,因此我们需要向客户端发送回多个 Feature

如你所见,我们的方法参数没有获取简单的请求和响应对象,而是获取一个请求对象(客户端想要在其中寻找 FeatureRectangle)和用于写响应的 RouteGuide_ListFeaturesServer 特殊对象。

在该方法中,我们填充多个我们需要返回的 Feature 对象,使用 RouteGuide_ListFeaturesServerSend() 方法将它们写到 RouteGuide_ListFeaturesServer。最后,就像在简单 RPC 中一样,我们返回 nil 错误,告诉 gRPC 我们已经完成写响应。如果在这个调用中发生任何错误,我们返回非 nil 错误;gRPC 层将其转换为适当的 RPC 状态,并且发送到网络上。

4.5.4. 客户端流 RPC

现在我们看客户端流方法 RecordRoute,我们从客户端获取 Point 流,返回包含行程信息的单个 RouteSummary。如你所见,这次方法根本没有请求参数。而是获取 RouteGuide_RecordRouteServer 流,服务端可以使用它读写消息 - 可以使用它的 Recv() 方法接收客户端消息,使用它的 SendAndClose() 返回单个响应。

在方法体中,我们使用 RouteGuide_RecordRouteServerRecv() 方法反复将客户端的请求读入到请求对象(在本例中是 Point)中,直到没有更多的消息:服务端需要在每次调用后检查 Recv() 返回的错误。如果为 nil,那么流仍然是好的,可以继续读取;如果为 io.EOF,那么消息流已经结束,服务端可以返回它的 RouteSummary。如果为任何其它值,我们将“原样”返回错误,以便 gRPC 层将其转换为 RPC 状态。

4.5.5. 双向流 RPC

最后我们看双向流 RPC RouteChat()

这次我们获取 RouteGuide_RouteChatServer 流,与客户端流示例一样,可使用它读写消息。但是,这次当客户端仍在向它们的消息流写消息时,我们通过方法的流返回值。

这里读写的语法与客户端流方法非常相似,不同之处是服务端使用流的 Send() 方法而不是 SendAndClose(),因为它要写入多个响应。

尽管每一方总是按照对方的写入顺序获得消息,但客户端和服务端都可以以任何顺序读取和写入消息 - 流完全独立地运行。

4.5.6. 启动服务端

在实现所有方法后,还需要启动 gRPC 服务,以便客户端可以实际使用我们的服务。下面的代码片段展示我们如何为 RouteGuide 服务做到这一点:

为构建和启动服务,我们:

  1. 指定我们想要使用的端口,监听客户端请求:lis, err := net.Listen(...)
  2. 使用 grpc.NewServer(...) 创建 gRPC 服务实例。
  3. 使用端口详细信息,调用服务上的 Serve(),来进行阻塞等待,直到进程被杀死或调用 Stop()

4.6. 创建客户端

接下来,我们看如何为 RouteGuide 服务创建 Go 客户端。你可以在 grpc-go/examples/route_guide/client/client.go 查看完整的示例客户端代码。

4.6.1. 创建存根

为调用服务方法,我们首先需要创建与服务端进行通信的 gRPC 通道。我们通过将服务端地址和端口号传递给 grpc.Dial() 的方式,创建它,如下所示:

如果服务需要认证,那么使用 DialOptionsgrpc.Dial 中设置认证凭据(例如 TLS、GCE 凭据或 JWT 凭据)。RouteGuide 服务不需要任何凭据。

设置 gRPC 通道后,我们需要客户端存根来执行 RPC。我们使用由示例 .proto 文件生成的 pb 包提供的 NewRouteGuideClient 方法来获取它。

4.6.2. 调用服务方法

接下来我们看如何调用服务方法。注意,在 gRPC-Go 中,RPC 以阻塞/同步模式运行,这意味着 RPC 调用等待服务端响应,并且将返回响应或错误。

4.6.2.1. 简单 RPC

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

如你所见,我们调用存根上的方法。在方法参数中,我们创建及填充请求 Protocol Buffer 对象(在本例中为 Point)。我们也传递一个 context.Context 对象,它使我们必要时改变 RPC 的行为,比如超时/取消运行中的 RPC。如果调用未返回错误,那么可以从服务端的第一个返回值中读取响应信息。

4.6.2.2. 服务端流 RPC

下面是我们调用服务端流方法 ListFeatures 的地方,该方法返回地理 Feature 流。在客户端和服务端,流 RPC 的实现方式类似:

与简单 RPC 中一样,我们给方法传递 context 和 request。但是,我们取回 RouteGuide_ListFeaturesClient 实例,而不是响应对象。客户端使用 RouteGuide_ListFeaturesClient 流读取服务端的响应。

我们使用 RouteGuide_ListFeaturesClientRecv() 方法反复地将服务端的响应读入到响应 Protocol Buffer 对象(在本例中为 Feature),直到没有更多的消息:客户端需要在每次调用后检查 Recv() 返回的错误 err。如果为 nil,那么流仍然是好的,可以继续读取;如果为 io.EOF,那么消息流已终止;否则,必须通过 err 传递 RPC 错误。

4.6.2.3. 客户端流 RPC

客户端流方法 RecordRoute 与服务端方法类似,不同之处在于我们只给方法传递上下文对象,并且取回 RouteGuide_RecordRouteClient 流,我们使用该流读写消息。

可以使用 RouteGuide_RecordRouteClientSend() 方法向服务端发送请求。我们使用 Send() 完成向流写入客户端请求后,需要调用流上的 CloseAndRecv(),让 gRPC 知道,我们已经完成写,期望接收响应。我们从 CloseAndRecv() 返回的 err 获取 RPC 状态。如果状态为 nil,那么 CloseAndRecv() 的第一个返回值是有效的服务端响应。

4.6.2.4. 双向流 RPC

RouteChat() 是双向流 RPC,像 RecordRoute() 一样,我们只给方法传递上下文对象,并且取回用于读写消息的流。但是,这次当服务端仍然向它们的消息流写消息时,我们通过方法的流返回值。

读写语法与客户端流方法非常相似,不同之处在于一旦我们完成调用,我们使用流的 CloseSend() 方法。尽管每一方总是按照对方的写入顺序获得消息,但客户端和服务端都可以以任何顺序读取和写入消息 - 流完全独立地运行。

4.6.3. 尝试一下

examples/route_guide 目录,运行如下命令:

  1. 运行服务端:

  2. 从另外一个终端,运行客户端:

将看到类似下面的输出(已省略时间戳):


参考文档