赞
踩
正如前文所述,gRPC应用通过RPC进行网络通信。作为一个gRPC应用的开发者,你无需知道RPC的实现细节,只需创建一个proto文件,并使用google提供的代码生成工具生成client/server端代码即可。所有低级别的实现细节都隐藏在生成的文件中,你只需处理高级别的抽象即可。然而,如果你想在生产环境构建基于gRPC的系统,了解它的实现原理是很有必要的。
本文主要讲解gRPC的通信方式是如何实现的,包括:
在一个RPC系统中,server端实现了一系列可供远程调用的函数,client端可以生成一个stub,它提供一系列与server端远程函数一一对应的函数,client端可以直接调用stub提供的函数,就像调用远程函数一样。
下图为client端调用远程函数的示例。server端提供getProduct函数来获取指定productID的产品详情,client端调用stub提供的对应的getProduct函数,来调用远程函数。整个调用过程如下:
application/grpc
。它调用的远程函数 ( /ProductInfo/getProduct
) 会作为单独的 HTTP 标头(path)发送;getProduct
,并将解码后的message作为参数传递给该函数。这些步骤与大多数 RPC 系统(如 CORBA、Java RMI 等)非常相似。gRPC 与它们之间的主要区别在于它对message进行编码的方式,它使用protocol buffer进行编码。
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
接下来我们看下如何使用protocol buffer对消息进行编码。
使用protocol buffer定义服务包括定义服务中的远程方法和定义我们希望通过网络发送的消息。
仍以ProductInfo
服务中的getProduct
函数为例。该getProduct
函数接受一个ProductID
消息作为输入参数并返回一个Product
消息。protocol buffer定义如下:
syntax = "proto3"; package ecommerce; service ProductInfo { rpc getProduct(ProductID) returns (Product); } message Product { string id = 1; string name = 2; string description = 3; float price = 4; } message ProductID { string value = 1; }
假设我们需要获取产品ID为15的产品详细信息,我们创建一个值为15的ProductID消息,并将其传递给getProduct函数。
product, err := c.GetProduct(ctx, &pb.ProductID{Value: “15”})
在下面的ProductID
消息结构中,有一个value
字段,其索引为 1。当我们创建一个value
等于15的消息实例时,生成的字节内容为该字段的字段标识符(field identifier)+value
具体的编码值。字段标识符有时也称为标签(tag):
message ProductID {
string value = 1;
}
字节内容结构如下图所示,其中,每个消息字段由一个标签值(Tag)和其编码值(Value)组成。
标签值由两部分组成,字段索引及其类型编号。字段索引是我们在 proto 文件中定义消息时分配给每个消息字段的唯一编号。类型编号是基于字段类型做的映射,可用于推测值的长度。下表显示了字段类型如何映射到类型编号的:
类型编号 | 类别 | 字段类型 |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
一旦我们知道了某个字段的字段索引和类型编号,我们就可以使用以下等式确定该字段的标签值:
Tag value = (field_index << 3) | type_number
回到我们之前的例子,ProductID消息有个字符串字段,其索引为1,字符串字段的类型编码为2,因此该字段对应的标签值即为10:
Tag value = (00000001 << 3) | 00000010
= 0001010
下一步是对消息字段的值进行编码。
Protocol buffer使用不同的编码技术对不同类型的数据进行编码。例如,如果它是一个字符串值,则Protocol buffer使用 UTF-8 对该值进行编码,如果它是一个 int32 字段类型的整数值,它使用一种称为 varints 的编码技术。我们在后文讲到具体的gRPC go语言实现时再讲这部分内容。
一旦我们有了要发送给对方的编码数据,我们就需要以对方可以轻松提取信息的方式打包数据,并通过网络发送给对方。gRPC 使用了一种称为长度前缀消息帧的技术来打包数据。
长度前缀是一种消息处理方式,它会在写入消息本身之前先写入每条消息的大小。如下图所示,在 gRPC 通信中,在编码的二进制消息之前会为每条消息分配 4 个额外的字节来设置其大小。消息的大小是一个有限的数字,分配 4 个字节来表示消息大小意味着 gRPC 通信可以处理最大 4 GB 的消息。
除了消息大小之外,消息帧还会预留一个 1 字节的无符号整数来指示数据是否被压缩。Compressed-Flag 值为 1 表示二进制数据使用 Message-Encoding 标头中声明的压缩方式进行压缩,值 0 表示没有对消息字节进行压缩。
现在消息已装帧并准备好通过网络发送给对应的处理方。对于client端请求消息,接收者是server。对于响应消息,接收者是client端。在接收端,一旦收到一条消息,首先需要读取第一个字节,检查消息是否被压缩。然后,读取接下来的四个字节以获取二进制消息的大小。一旦知道大小,就可以从消息流中读取具体的消息了。对于简单消息,我们只需处理一条带长度前缀的消息,而对于流式消息,我们需要处理多条带长度前缀的消息。
HTTP/2 是 Internet 协议 HTTP 的第二个主要版本。引入它是为了克服以前版本(HTTP/1.1)中遇到的一些安全、速度等问题。HTTP/2 以一种更有效的方式支持 HTTP/1.1 的所有核心功能。因此,用 HTTP/2 编写的应用程序更快、更简单、更健壮。
gRPC 使用 HTTP/2 作为其传输协议通过网络发送消息。这也是 gRPC 是高性能 RPC 框架的原因之一。
如下图所示,gRPC Channel表示client端与server端之间的连接,即 HTTP/2 连接。当client端创建 gRPC Channel时,它会在后台创建与server端的 HTTP/2 连接。创建好Channel后,我们可以重用它来向server端发送多个远程调用,这些远程调用会映射到 HTTP/2 的流中。在远程调用中发送的消息以 HTTP/2 帧的形式发送。一个帧可能携带一个 gRPC 长度前缀消息,如果一个 gRPC 消息非常大,它可能跨越多个数据帧。
在上一节中,我们讨论了如何将我们的消息帧转为以长度为前缀的消息。当我们通过网络将它们作为请求或响应消息发送时,我们需要随消息一起发送额外的HTTP头。下面,我们看看如何构造请求/响应消息,以及需要为每条消息传递哪些标头。
请求消息是发起远程调用的消息。在 gRPC 中,请求消息总是由client端应用程序触发,它由三个主要部分组成:请求头、长度前缀消息和流结束标志,如下图所示。client端首先发送请求头,之后是长度前缀消息,最后是EOS,标识消息发送完毕。
下面仍以ProductInfo
服务中的getProduct
函数为例,来解释请求消息是如何在 HTTP/2 帧中发送的。
当我们调用该getProduct
函数时,client端会发送以下请求头:
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
:method:设置 HTTP 方法。对于 gRPC,:method
标头始终为POST
.
:scheme:设置 HTTP 协议。如果启用了 TLS,则协议设置为“https”,否则为“http”。
:path:设置终端路径。对于 gRPC,此值构造为“/{服务名称}/{方法名称}"。
:authority:设置目标 URI 的虚拟主机名。
te:设置不兼容代理的检测。对于 gRPC,该值必须是“trailers”。
grpc-timeout:设置调用超时时常。如果未指定,server端应假定无限超时。
content-type:设置内容类型。对于 gRPC,内容类型应以application/grpc
. 如果没有,gRPC server会响应 HTTP 状态 415(不支持的媒体类型)。
grpc-encoding:设置消息压缩方式。可能的值为identity
、gzip
、deflate
、snappy
及自定义压缩方式。
authorization:这是可选的请求头,用于访问又安全限制的终端服务。
一旦client端发起与server端的调用,client端就会以 HTTP/2 数据帧的形式发送带有长度前缀的消息。如果一个数据帧无法放下长度前缀消息,它可以跨越多个数据帧。通过在最后一个数据帧上添加一个END_STREAM
标志来标识请求消息的结束。当没有数据要发送但我们需要关闭请求流时,我们需要发送一个带有END_STREAM
标志的空数据帧:
DATA (flags = END_STREAM)
<Length-Prefixed Message>
响应消息由server端响应client端的请求而生成。与请求消息类似,在大多数情况下,响应消息也由三个主要部分组成:响应标头、带长度前缀的消息和尾部。当响应中没有以长度为前缀的消息需要发送给client端时,响应消息仅包含响应标头和尾部。
继续回到上一个例子。当server端向client端发送响应时,它首先发送响应头,如下所示:
HEADERS (flags = END_HEADERS)
:status = 200
grpc-encoding = gzip
content-type = application/grpc
identity
、gzip
、deflate
、snappy
和自定义类型。一旦server端发送完响应标头,就会以 HTTP/2 数据帧的形式发送带有长度前缀的消息。与请求消息类似,如果一个数据帧无法放下长度前缀消息,它可以跨越多个数据帧:
DATA
<Length-Prefixed Message>
与请求消息不同的是,END_STREAM
标志不随数据帧一起发送,它作为一个单独的响应头发送(被称作Trailers),通知client端我们完成了响应消息的发送。Trailers 还会携带请求的状态码和状态消息:
HEADERS (flags = END_STREAM, END_HEADERS)
grpc-status = 0 # OK
grpc-message = xxxxxx
在某些情况下,请求调用可能会立即失败。在这些情况下,server端需要在没有数据帧的情况下发回响应。此时,server端只会发送Trailers作为响应。
gRPC 支持四种通信模式,分别是简单 RPC、服务端流式 RPC、客户端流式 RPC 和双向流式 RPC。
在简单的 RPC 中,在 gRPC server端和 gRPC client端之间的通信总是一个请求对应一个响应。如下图所示。请求消息包含请求头,后跟长度前缀消息,该消息可以跨越一个或多个数据帧,并在消息结尾处设置流结束(EOS)标志,以在client端半关闭连接并标记请求消息的结束。这里的“半关闭连接”是指client端关闭其一侧的连接,因此client端不再能够向server端发送消息,但仍然可以监听来自server端的传入消息。server端只有在收到完整的消息后才创建响应消息。响应消息包含一个响应头,后跟一个长度前缀的消息。一旦server端发送带有状态详细信息的Trailers标头,通信就会结束。
从client端的角度来看,简单 RPC 和服务端流式 RPC 具有相同的请求消息流。在这两种情况下,我们都会发送一条请求消息。主要区别在于server端。server端会发送多条消息,而不是向client端发送一条响应消息。server端一直等待,直到收到完整的请求消息,之后发送响应头和多个带长度前缀的消息。一旦server端发送带有状态详细信息的Trailers标头,通信就会结束。
在客户端流式 RPC 中,client端向server端发送多条消息,server端发送一条响应消息作为回复。client端首先通过发送请求头帧建立与server端的连接。建立连接后,client端会向server端发送多个长度前缀消息作为数据帧。最后,client端通过在最后一个数据帧中发送一个 EOS 标志来半关闭连接。同时,server端读取从client端接收到的消息。一旦接收到所有消息,server端就会发送响应消息以及Trailers标头并关闭连接。
在此模式中,client端通过发送请求头帧来建立连接。一旦建立连接,client端和server端都可以直接发送多个长度前缀消息,而无需等待对方完成。双方都可以自主结束连接,这意味着他们不能再发送任何消息。
如下图所示,gRPC 采用分层架构实现。
最基础的一层是 gRPC Core层,它从上层抽象了所有网络操作,以便应用程序开发人员可以轻松地进行 RPC 开发。Core层还提供核心功能的扩展,包括安全认证等。
gRPC 原生支持 C/C++、Go 和 Java 语言。gRPC 还提供许多流行语言的语言绑定,例如 Python、Ruby、PHP 等。这些语言绑定是对低级 C API 的包装。
应用程序代码位于语言绑定之上。该应用层处理应用逻辑和数据编码逻辑。通常,开发人员使用不同语言提供的编译器为数据编码逻辑生成源代码。例如,如果我们使用protocol buffer对数据进行编码,那么protocol buffer编译器可以用于生成源代码。开发人员可以在他们的应用层逻辑中调用生成的源代码的方法。
gRPC 建立在两个快速高效的协议之上,称为protocol buffer和 HTTP/2。protocol buffer是一种数据序列化协议,它是一种与语言无关、平台中立和可扩展的结构化数据序列化方法。序列化后,此协议会生成一个比普通 JSON 数据更小的二进制强类型数据。之后,这个序列化后的二进制数据会通过称为 HTTP/2 的二进制传输协议进行传输。
HTTP/2 是互联网协议 HTTP 的下一个主要版本。HTTP/2 是完全多路复用的,这意味着 HTTP/2 可以通过单个 TCP 连接并行发送多个数据请求。这使得用 HTTP/2 编写的应用程序比其他应用程序更快、更简单、更健壮。
所有这些因素使 gRPC 成为一个高性能的 RPC 框架。
大厂搬砖人员,有多年的微服务开发经验,精通Go、Python语言。欢迎关注我的公众号,x的技术杂谈。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。