gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。
gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。
这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
gRPC 一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。
gRPC 是什么?
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。
与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。
在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。
在客户端拥有一个存根能够像服务端一样的方法。
gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。
所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。
此外,Google 最新 API 将有 gRPC 版本的接口,使你很容易地将 Google 的功能集成到你的应用里。
gRPC 概念
通过对于 gRPC 的架构和 RPC 生命周期的概览来介绍 gRPC 的主要概念。
服务定义
正如其他 RPC 系统,gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。
gRPC 默认使用 protocol buffers 作为接口定义语言,来描述服务接口和有效载荷消息结构。如果有需要的话,可以使用其他替代方案。
gRPC 允许你定义四类服务方法:
- 单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
- 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
- 客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
- 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
使用 API 接口
gRPC 提供 protocol buffer 编译插件,能够从一个服务定义的 .proto 文件生成客户端和服务端代码。通常 gRPC 用户可以在服务端实现这些API,并从客户端调用它们。
- 在服务侧,服务端实现服务接口,运行一个 gRPC 服务器来处理客户端调用。gRPC 底层架构会解码传入的请求,执行服务方法,编码服务应答。
- 在客户侧,客户端有一个存根实现了服务端同样的方法。客户端可以在本地存根调用这些方法,用合适的 protocol buffer 消息类型封装这些参数— gRPC 来负责发送请求给服务端并返回服务端 protocol buffer 响应。
同步 vs 异步
同步 RPC 调用一直会阻塞直到从服务端获得一个应答,这与 RPC 希望的抽象最为接近。另一方面网络内部是异步的,并且在许多场景下能够在不阻塞当前线程的情况下启动 RPC 是非常有用的。
在多数语言里,gRPC 编程接口同时支持同步和异步的特点。你可以从每个语言教程和参考文档里找到更多内容(很快就会有完整文档)。
RPC 生命周期
单项 RPC
首先我们来了解一下最简单的 RPC 形式:客户端发出单个请求,获得单个响应。
- 一旦客户端通过桩调用一个方法,服务端会得到相关通知 ,通知包括客户端的元数据,方法名,允许的响应期限(如果可以的话)
- 服务端既可以在任何响应之前直接发送回初始的元数据,也可以等待客户端的请求信息,到底哪个先发生,取决于具体的应用。
- 一旦服务端获得客户端的请求信息,就会做所需的任何工作来创建或组装对应的响应。如果成功的话,这个响应会和包含状态码以及可选的状态信息等状态明细及可选的追踪信息返回给客户端 。
- 假如状态是 OK 的话,客户端会得到应答,这将结束客户端的调用。
服务端流式 RPC
服务端流式 RPC 除了在得到客户端请求信息后发送回一个应答流之外,与我们的简单例子一样。在发送完所有应答后,服务端的状态详情(状态码和可选的状态信息)和可选的跟踪元数据被发送回客户端,以此来完成服务端的工作。客户端在接收到所有服务端的应答后也完成了工作。
客户端流式 RPC
客户端流式 RPC 也基本与我们的简单例子一样,区别在于客户端通过发送一个请求流给服务端,取代了原先发送的单个请求。服务端通常(但并不必须)会在接收到客户端所有的请求后发送回一个应答,其中附带有它的状态详情和可选的跟踪数据。
双向流式 RPC
双向流式 RPC ,调用由客户端调用方法来初始化,而服务端则接收到客户端的元数据,方法名和截止时间。服务端可以选择发送回它的初始元数据或等待客户端发送请求。 下一步怎样发展取决于应用,因为客户端和服务端能在任意顺序上读写 - 这些流的操作是完全独立的。例如服务端可以一直等直到它接收到所有客户端的消息才写应答,或者服务端和客户端可以像"乒乓球"一样:服务端后得到一个请求就回送一个应答,接着客户端根据应答来发送另一个请求,以此类推。
截止时间
gRPC 允许客户端在调用一个远程方法前指定一个最后期限值。这个值指定了在客户端可以等待服务端多长时间来应答,超过这个时间值 RPC 将结束并返回DEADLINE_EXCEEDED
错误。在服务端可以查询这个期限值来看是否一个特定的方法已经过期,或者还剩多长时间来完成这个方法。 各语言来指定一个截止时间的方式是不同的 - 比如在 Python 里一个截止时间值总是必须的,但并不是所有语言都有一个默认的截止时间。
RPC 终止
在 gRPC 里,客户端和服务端对调用成功的判断是独立的、本地的,他们的结论可能不一致。这意味着,比如你有一个 RPC 在服务端成功结束("我已经返回了所有应答!"),到那时在客户端可能是失败的("应答在最后期限后才来到!")。也可能在客户端把所有请求发送完前,服务端却判断调用已经完成了。
取消 RPC
无论客户端还是服务端均可以再任何时间取消一个 RPC 。一个取消会立即终止 RPC 这样可以避免更多操作被执行。它不是一个"撤销", 在取消前已经完成的不会被回滚。当然,通过同步调用的 RPC 不能被取消,因为直到 RPC 结束前,程序控制权还没有交还给应用。
元数据集
元数据是一个特殊 RPC 调用对应的信息(授权详情]) ,这些信息以键值对的形式存在,一般键的类型是字符串,值的类型一般也是字符串(当然也可以是二进制数据)。元数据对 gRPC 本事来说是不透明的 - 它让客户端提供调用相关的信息给服务端,反之亦然。 对于元数据的访问是语言相关的。
流控制
TBD
配置
TBD
频道
在创建客户端存根时,一个 gRPC 频道提供一个特定主机和端口服务端的连接。客户端可以通过指定频道参数来修改 gRPC 的默认行为,比如打开关闭消息压缩。一个频道具有状态,包含已连接
和空闲
。 gRPC 如何处理关闭频道是语言相关的。有些语言可允许询问频道状态。
安全认证
gRPC 被设计成可以利用插件的形式支持多种授权机制。本文档对多种支持的授权机制提供了一个概览,并且用例子来论述对应API,最后就其扩展性作了讨论。
支持的授权机制
SSL/TLS
gRP 集成 SSL/TLS 并对服务端授权所使用的 SSL/TLS 进行了改良,对客户端和服务端交换的所有数据进行了加密。
对客户端来讲提供了可选的机制提供凭证来获得共同的授权。
OAuth 2.0
gRPC 提供通用的机制(后续进行描述)来对请求和应答附加基于元数据的凭证。
当通过 gRPC 访问 Google API 时,会为一定的授权流程提供额外的获取访问令牌的支持,这将通过以下代码例子进行展示。 警告:Google OAuth2 凭证应该仅用于连接 Google 的服务。把 Google 对应的 OAuth2 令牌发往非 Google 的服务会导致令牌被窃取用作冒充客户端来访问 Google 的服务。
API
为了减少复杂性和将混乱最小化, gRPC 以一个统一的凭证对象来进行工作。 凭证可以是以下两类:
-
频道凭证, 被附加在
频道
上, 比如 SSL 凭证。 -
调用凭证, 被附加在调用上(或者 C++ 里的
客户端上下文
)。- 凭证可以用
组合频道凭证
来进行组合。 - 一个
组合频道凭证
可以将一个频道凭证
和一个调用凭证
关联创建一个新的频道凭证
。 - 结果在这个频道上的每次调用会发送组合的
调用凭证
来作为授权数据。- 例如,一各
频道凭证
可以由一个Ssl 凭证
和一个访问令牌凭证
生成。结果是在这个频道上的每次调用都会发送对应的访问令牌。调用凭证
可以用组合凭证
来组装。组装后的调用凭证
应用到一个客户端上下文
里,将触发发送这两个调用凭证
的授权数据。
- 例如,一各
- 凭证可以用
通讯协议
HTTP2 协议上的 gRPC
大纲
以下是 gRPC 请求和应答消息流中一般的消息顺序:
- 请求 → 请求报头 *有定界符的消息 EOS
- 应答 → 应答报头 *有定界符的消息 EOS
- 应答 → (应答报头 *有定界符的消息 跟踪信息) / 仅仅跟踪时
请求
请求 → 请求报头 *界定的消息 EOS 请求报头是通过报头+联系帧方式以 HTTP2 报头来发送的。
请求报头 → 调用定义 *自定义元数据
调用定义 → 方法模式路径TE [授权] [超时] [内容类型] [消息类型] [消息编码] [接受消息类型] [用户代理]
方法 → “:method POST”
模式 → “:scheme ” (“http” / “https”)
路径 → “:path” {开放的 API 对应的方法路径}
Authority → “:authority” {授权的对应的虚拟主机域名}
TE → “te” “trailers” # 用来检测不兼容的代理
超时 → “grpc-timeout” 超时时间值 超时时间单位
超时时间值 → {至少8位数字正整数的 ASCII 码字符串}
超时时间单位 → 时 / 分 / 秒 / 毫秒 / 微秒 / 纳秒
时 → “H”
分 → “M”
秒 → “S”
毫秒 → “m”
微秒 → “u”
纳秒 → “n”
内容类型 → “content-type” “application/grpc” [(“+proto” / “+json” / {自定义})]
内容编码 → “gzip” / “deflate” / “snappy” / {自定义}
消息编码 → “grpc-encoding” Content-Coding
接受消息编码 → “grpc-accept-encoding” Content-Coding *("," Content-Coding)
用户代理 → “user-agent” {结构化的用户代理字符串}
消息类型 → “grpc-message-type” {消息模式的类型名}
自定义数据 → 二进制报头 / ASCII 码报头
二进制报头 → {以“-bin”结尾小写的报头名称的 ASCII 码 } {以 base64 进行编码的值}
ASCII 码报头 → {小写报头名称的 ASCII 码} {值}
界定的消息的重复序列通过数据帧来进行传输。
界定的消息 → 压缩标志 消息长度 消息
压缩标志 → 0 / 1 # 编码为 1 byte 的无符号整数
消息长度 → {消息长度} # 编码为 4 byte 的无符号整数
消息 → *{二进制字节}
应答
应答 → (应答报头 界定的消息 跟踪信息) / 仅仅跟踪
应答报头 → HTTP 状态 [消息编码] [消息接受编码] 内容类型 *自定义元数据
仅仅跟踪 → HTTP 状态 内容类型 跟踪消息
跟踪消息 → 状态 [状态消息] *自定义元数据
HTTP状态 → “:status 200”
状态 → “grpc-status” <状态码的 ASCII 字符串>
状态消息 → “grpc-message” <状态描述文本对应的 ASCII 字符串>
HTTP2 传输映射
流识别
所有的 GRPC 调用需要定义指定一个内部 ID。我们将在这个模式里使用 HTTP2 流 ID 来作为调用标识。注意:这些 ID 在一个打开的 HTTP2 会话里是前后关联的,在一个处理多个 HTTP2 会话的进程里不是唯一的,也不能被用作 GUID。
数据帧
数据帧边界与界定消息的边界无关,实现时不应假定它们有一致性。
错误
当应用错误或运行时错误在 PRC 调用过程中出现时,状态和状态消息应当通过跟踪消息发送。
安全
HTTP2 规范当使用 TLS 时强制使用 TLS 1.2 及以上的版本,并且在部署上对允许的密码施加一些额外的限制以避免已知的比如需要 SNI 支持的问题。并且期待 HTTP2 与专有的传输安全机制相结合,这些传输机制的规格说明不能提供有意义的建议。
连接管理
GOAWAY 帧
服务端发出这种帧给客户端表示服务端在相关的连接上不再接受任何新流。这种帧包含服务端最后成功接受的流的ID。客户端应该认为任何在最后成功的流后面初始化的任意流为 UNAVAILABLE,并且在别处重试这些调用。客户端可以*地在已经接受的流上继续工作直到它们完成或者连接中断。 服务端应该在终止连接前发送 GOAWAY 帧,以可靠地通知客户端哪些工作已经被服务端接受并执行。
PING 帧
客户端和服务端均可以发送一个 PING 帧,对方必须精确回显它们所接收到的信息。这可以被用来确认连接仍然是活动的,并且能够提供估计端对端延迟估计的方法。假如服务端初始的 PING 在最后期限仍然没有收到运行时所期待的应答的话,所有未完成的调用将会被以取消状态关闭。一个客户端期满的初始的PING则会导致所有的调用被以用不可用状态关闭。注意PING的频率高度依赖于网络环境,实现可以根据网络和应用需要,*地调整PING频率。
连接失败
假如客户端检测到连接失败,所有的调用都会被以不可用状态关闭。而服务端侧则所有已经打开的调用都会被以取消状态关闭。
- 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。