HTTP/2 是互联网协议 HTTP 的第 2 个主版本。之所以引入它,是为了解决以前版本(HTTP/1.1)在安全性、速度等方面所遇到的问题。

HTTP/2 支持 HTTP/1.1 所有的核心特性,只不过实现方式更高效。因此,使用 HTTP/2 编写的应用程序更快、更简单,也更健壮。

gRPC 使用 HTTP/2 作为其传输协议,实现通过网络发送消息。这也是gRPC 能够成为高性能 RPC 框架的原因之一。接下来探索 gRPC 和HTTP/2 的关系。

在 HTTP/2 中,客户端和服务器端的所有通信都是通过一个TCP 连接完成的,这个连接可以传送任意数量的双向字节流。为了理解 HTTP/2 的过程,最好熟悉下面这些重要术语。

  • 流(stream):在一个已建立的连接上的双向字节流。一个流可以携带一条或多条消息。
  • 帧(frame):HTTP/2 中最小的通信单元。每一帧都包含一个帧头,它至少要标记该帧所属的流。
  • 消息(message):完整的帧序列,映射为一条逻辑上的 HTTP消息,由一帧或多帧组成。这样的话,允许消息进行多路复用,客户端和服务器端能够将消息分解成独立的帧,交叉发送它们,然后在另一端进行重新组合。

如图 4-5 所示,gRPC 通道代表一个到端点的连接,也就是一个 HTTP/2连接。当客户端应用程序创建 gRPC 通道的时候,它会在幕后创建一个到服务器端的 HTTP/2 连接。在通道创建完成之后,就可以重用它来发送多个到服务器端的远程调用。这些远程调用会映射为 HTTP/2 中的流。远程调用中的消息以 HTTP/2 帧的形式进行发送,帧可能会携带一条 gRPC 长度前缀的消息,也可能在 gRPC 消息非常大的情况下,一条消息跨多帧。

图 4-5:gRPC 语义与 HTTP/2 之间的关系

4.3 节讨论了基于长度前缀的消息分帧。当把这些消息以请求消息或响应消息的形式通过网络进行发送时,除了消息本身,还要发送额外的头信息。下面讨论如何组织请求消息和响应消息,以及针对每条消息所要传递的头信息。

4.4.1 请求消息

请求消息用于初始化远程调用。在 gRPC 中,请求消息始终由客户端应用程序来触发,它包含 3 部分:请求头信息、以长度作为前缀的消息以及流结束标记(end of stream flag,以下简称 EOS 标记),如图 4-6 所示。远程调用在客户端发送请求头信息之后就会初始化,然后其中会发送以长度作为前缀的消息,最后发送 EOS 标记,通知收件方请求消息已发送。

图 4-6:请求消息中的消息元素序列

这里可以再次使用 ProductInfo 服务中的 getProduct 方法,来理解请求消息在 HTTP/2 帧中的发送方式。当调用 getProduct 方法时,客户端会通过发送下面的请求头信息来初始化调用。

HEADERS (flags = END_HEADERS)
:method = POST ➊
:scheme = http ➋
:path = /ProductInfo/getProduct ➌
:authority = abc.com ➍
te = trailers ➎
grpc-timeout = 1S ➏
content-type = application/grpc ➐
grpc-encoding = gzip ➑
authorization = Bearer xxxxxx ➒

❶ 定义 HTTP 方法。对 gRPC 来说,:method 头信息始终为 POST。
❷ 定义 HTTP 模式。如果启用传输层安全协议(Transport LevelSecurity,TLS),就将模式设置为 https,否则设置为 http。
❸ 定义端点路径。对 gRPC 来说,这个值的构造为 /{ 服务名 }/{ 方法名 }。
❹ 定义目标 URI 的虚拟主机名。
❺ 定义对不兼容代理的检测。在 gRPC 中,这个值必须为 trailers。
❻ 定义调用的超时时间。如果没有指定,服务器端会假定超时时间无穷大。
❼ 定义 content-type。对 gRPC 来说,content-type 应该以application/grpc 开头。否则,gRPC 会给出 HTTP 状态为 415(不支持的媒体类型)的响应。
❽ 定义消息的压缩类型。可选的值是identity、gzip、deflate、snappy 和 {custom}。
❾ 这是可选的元数据。authorization 元数据用来访问安全的端点。

在本例中,还有其他几点需要注意。

  • 名称以“:”开头的头信息叫作保留头信息,HTTP/2 要求保留头信息出现在其他头信息之前。
  • gRPC 通信中所传递的头信息分为两类:调用定义的头信息(call-definition header)和自定义元数据。
  • 调用定义的头信息是 HTTP/2 预定义的头信息。这些头信息应该在自定义元数据之前发送。
  • 自定义元数据是由应用程序层定义的任意一组键–值对。在声明自定义元数据时,需要确保不要使用以 grpc- 开头的名称。在 gRPC 核心中,这被列为保留名字。

当完成对服务器端调用的初始化之后,客户端会以 HTTP/2 数据帧的形式发送以长度作为前缀的消息。如果这条消息不适合放到一个数据帧中,那么它可以跨多个数据帧。请求消息的结束通过在最后一个 DATA帧上添加 END_STREAM 标记来实现。当因为没有要发送的数据而需要关闭请求流时,必须发送一个带有 END_STREAM 标记的空数据帧:

DATA (flags = END_STREAM)
<Length-Prefixed Message>

这里只是大致介绍了 gRPC 请求消息的结构,你可以通过 gRPC 官方的GitHub 仓库了解更多细节。

与请求消息类似,响应消息也有其自身的结构。接下来看一下响应消息的结构及其关联的头信息。

4.4.2 响应消息

响应消息由服务器端生成,用来响应客户端的请求。与请求消息类似,在大多数场景中,响应消息也包含 3 个主要部分:响应头信息、以长度作为前缀的消息以及 trailer。如果没有发送以长度作为前缀的消息来响应客户端,则响应消息只会包含头信息和 trailer,如图 4-7 所示。

图 4-7:响应消息中的消息元素序列

下面通过同一个示例来介绍响应消息的 HTTP/2 帧序列。当服务器端发
送响应消息至客户端时,首先会发送如下所示的响应头信息。

HEADERS (flags = END_HEADERS)
:status = 200 ➊
grpc-encoding = gzip ➋
content-type = application/grpc ➌

❶ 表明 HTTP 请求的状态。
❷ 定义消息的压缩类型。可选的值是identity、gzip、deflate、snappy 和 {custom}。
❸ 定义 content-type。对 gRPC 来说,content-type 应该以application/grpc 开头。

与请求头信息类似,应用程序层所定义的自定义元数据也可以按照任意键–值对集的形式在响应头信息中进行发送。

服务器端发送完响应头之后,以长度作为前缀的消息就会以 HTTP/2 数据帧的形式在调用中进行发送。与请求消息类似,如果该消息不适合放到一个数据帧中,那么它可以跨多个数据帧。如下所示,END_STREAM标记并不会随数据帧一起发送,而会作为单独的头信息来发送,名为trailer:

DATA
<Length-Prefixed Message>

最后,通过发送 trailer 来提醒客户端响应消息已发送。trailer 还会携带状态码以及请求的状态信息。

HEADERS (flags = END_STREAM, END_HEADERS)
grpc-status = 0 # OK ➊
grpc-message = xxxxxx ➋

❶ 定义 gRPC 状态码。gRPC 会使用一组定义良好的状态码。这些状态码的定义可以在 gRPC 官方文档中找到。
❷ 定义对错误的描述。这是可选的,只有在处理请求出现错误时,才会进行设置。

trailer 会以 HTTP/2 头信息帧的形式进行投递,但会在响应消息结束时发送。响应 EOS 标记就是在 trailer 头信息中设置的END_STREAM 标记。另外,它还会包含 grpc-status 头信息和grpc-message 头信息。

在特定的场景中,请求调用可能会立即失败。在这些情况下,服务器端需要发回一个不包含数据帧的响应。因为服务器端只发送 trailer 作为响应,所以这些 trailer 也会以 HTTP/2 头信息帧的形式进行投递,同时会包含 END_STREAM 标记。另外,trailer 会包含下面的头信息。

  • HTTP 状态::status
  • 内容类型:content-type
  • 状态:grpc-status
  • 状态信息:grpc-message

现在,我们已经知道了 gRPC 消息如何在 HTTP/2 连接上流动,接下来看不同通信模式的消息流。

4.4.3 理解gRPC通信模式中的消息流

第 3 章介绍了 gRPC 支持的 4 种通信模式,即一元 RPC 模式、服务器端流 RPC 模式、客户端流 RPC 模式以及双向流 RPC 模式,也讨论了这些通信模式在现实场景中的运行方式。本节将从不同的角度再来看一下这些模式,并结合本章内容,讨论每种模式在传输层中的运行方式。

01. 一元 RPC 模式

在一元 RPC 模式中,gRPC 服务器端和 gRPC 客户端的通信始终只涉及一个请求和一个响应。如图 4-8 所示,请求消息包含头信息,随后是以长度作为前缀的消息,该消息可以跨一个或多个数据帧。

消息最后会添加一个 EOS 标记,方便客户端半关(half-close)连接,并标记请求消息的结束。在这里,“半关”指的是客户端在自己的一侧关闭连接,这样一来,客户端无法再向服务器端发送消息,但仍能够监听来自服务器端的消息。只有在接收到完整的消息之后,服务器端才生成响应。响应消息包含一个头信息帧,随后是以长度作为前缀的消息。当服务器端发送带有状态详情的 trailer 头信息之后,通信就会关闭。

图 4-8:一元 RPC 模式:消息流

这是最简单的通信模式。接下来看一下较为复杂的服务器端流 RPC模式。

02. 服务器端流 RPC 模式

从客户端的角度来说,一元 RPC 模式和服务器端流 RPC 模式具有相同的请求信息流。这两种情况都是发送一条请求消息,主要差异在于服务器端。在服务器端流 RPC 模式中,服务器端不再向客户端发送一条响应消息,而会发送多条响应消息。服务器端会持续等待,直到接收到完整的请求消息,随后它会发送响应头消息和多条以长度作为前缀的消息,如图 4-9 所示。在服务器端发送带有状态详情的 trailer 头信息之后,通信就会关闭。

图 4-9:服务器端流 RPC 模式:消息流

下面看一下客户端流 RPC 模式,它在很大程度上与服务器端流RPC 模式相反。

03. 客户端流 RPC 模式

在客户端流 RPC 模式中,客户端向服务器端发送多条消息,服务器端在响应时发送一条消息。客户端首先通过发送头信息帧来与服务器端建立连接,然后以数据帧的形式,向服务器端发送多条以长度作为前缀的消息,如图 4-10 所示。最后,通过在末尾的数据帧中发送 EOS 标记,客户端将连接设置为半关的状态。与此同时,服务器端读取所接收到的来自客户端的消息。在接收到所有的消息之后,客户端发送一条响应消息和 trailer 头信息,并关闭连接。

图 4-10:客户端流 RPC 模式:消息流

接下来看最后一种通信模式,也就是双向流 RPC 模式。在这种模式中,客户端和服务器端都会给对方发送多条消息,直到它们关闭连接为止。

04. 双向流 RPC 模式

在双向流 RPC 模式中,客户端通过发送头信息帧与服务器端建立连接。然后,它们会互发以长度作为前缀的消息,无须等待对方结束。如图 4-11 所示,客户端和服务器端会同时发送消息。两者都可以在自己的一侧关闭连接,这意味着它们不能再发送消息了。

图 4-11:双向流 RPC 模式:消息流

至此,深入学习 gRPC 通信的旅程即将结束。网络以及通信中有关传输的操作通常是在 gRPC 核心层处理的,但 gRPC 应用程序开发人员无须关注这些细节。

在结束本章之前,让我们来看一下 gRPC 的实现架构和语言栈。

文档更新时间: 2023-09-02 05:27   作者:Minho