如前文所述,gRPC 使用 protocol buffers 编写服务定义。使用 protocolbuffers 定义服务,具体包括定义服务中的远程方法以及希望通过网络发送的消息。以 ProductInfo 服务中的 getProduct 方法为例,该方法接受 ProductID 消息作为输入参数,并返回 Product 消息。这里可以将输入和输出的消息结构使用 protocol buffers 进行定义,如代码清单 4-1 所示。

代码清单 4-1 getProduct 方法的服务定义

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;
}

如代码清单 4-1 所示,由于 ProductID 消息带有唯一的商品 ID,因此它只有一个字符串类型的字段,Product 消息则具有表示商品所需的结构。正确地定义消息非常重要,这决定了消息该如何进行编码。本节稍后将讨论在编码消息时如何使用消息定义。

有了消息定义之后,接下来看一下如何编码消息,并生成与之对等的字节内容。在正常情况下,这是由消息定义生成的源代码处理的。所有支持的语言都有自己的编译器来生成源代码,应用程序开发人员则需要将消息定义传递进去,从而生成读取消息和写入消息的源代码。

假设需要根据商品 ID(15)来获取商品详情,那么可以创建一个值为15 的消息对象,并将其传递给 getProduct 方法。如下的代码片段展示了如何创建值为 15 的 ProductID 消息,并将其传递给 getProduct方法,从而获取商品详情:

product, err := c.GetProduct(ctx, &pb.ProductID{Value: "15"})

这个代码片段是用 Go 语言编写的,ProductID 消息的定义位于生成的代码之中。我们创建了一个 ProductID 实例,并将它的值设置为 15。

Java 语言的实现与之类似,下面使用生成的方法来创建 ProductID 实例:

ProductInfoOuterClass.Product product = stub.getProduct(ProductInfoOuterClass.ProductID.newBuilder().setValue("15").build());

在接下来要讨论的 ProductID 消息结构中,有一个名为 value 的字段,并且字段索引为 1。当创建 value 值为 15 的消息实例时,对应的字节内容会包含一个用于 value 字段的标识符,随后是其编码后的值。字段的标识符也被称为标签(tag):

message ProductID {
    string value = 1;
}

这个字节内容的结构如图 4-2 所示,其中每个字段包含一个字段标识符及其编码后的值。

图 4-2:protocol buffers 编码后的字节流

标签由两个值构成:字段索引和线路类型(wire type)。字段索引就是在 proto 文件中定义消息时,为每个消息字段所设置的唯一数字。线路类型是基于字段类型的,也就是能够为字段输入值的数据类型。线路类型会提供信息来确定值的长度。表 4-1 展示了线路类型如何映射为字段类型,这些都是预定义的线路类型和字段类型的映射。可以参考protocol buffers 编码的官方文档来获取关于映射的更多信息。

表4-1:可用的线路类型及其对应的字段类型

线路类型 分类 字段类型
0 Varint int32、int64、uint32、uint64、sint32、sint64、bool、enum
1 64 位 fixed64、sfixed64、double
2 基于长度分隔 string、bytes、嵌入式消息、打包的 repeated 字段
3 起始组 groups(已废弃)
4 结束组 groups(已废弃)
5 32 位 fixed32、sfixed32、float

了解了特定字段的字段索引和线路类型后,就可以使用下面的公式来确定其标签的值。这里将表示字段索引的二进制左移 3 位并与表示线路类型的值进行按位或操作:

Tag value = (field_index << 3) | wire_type

图 4-3 展示了字段索引和线路类型在标签值中的排列方式。

图 4-3:标签值的结构

可以通过前面的例子来进一步了解标签值这个术语。ProductID 有一个字符串字段,该字段的索引为 1,并且字符串的线路类型为 2。在将其转换为二进制表述时,字段索引将是 00000001,线路类型将是00000010。将这些值代入公式,可以按照如下的方式得到值为 10 的标签值:

Tag value = (00000001 << 3) | 00000010 = 000 1010

下一步就是编码消息字段的值。protocol buffers 使用不同的编码技术来编码不同类型的数据。对于字符串值,protocol buffers 会使用 UTF-8 对值进行编码;对于 int32 字段类型的整型值,它会使用名为 Varint 的编码技术。下面的小节将详细讨论不同的编码技术以及何时使用这些技术。下面看一下如何对字符串值进行编码,从而完成该示例。

在 protocol buffers 编码中,字符串值会使用 UTF-8 编码技术来进行编码。UTF(Unicode Transformation Format)使用 8 位的块来表示一个字符。它是一种长度可变的字符编码技术,也是 Web 页面和电子邮件首选的编码技术。

在这个示例的 ProductID 消息中,value 字段的值为 15,15 对应的UTF-8 编码值为 \x31\x35。换句话说,表述编码值所需的 8 位块的数量并不是固定的,它会根据消息字段值的变化而变化。在这里,它会有2 块。因此,我们需要在编码值之前传递编码值的长度,也就是编码值所要跨的块数。编码值为 15 的十六进制表示如下所示:

A 02 31 35

在这里,右侧的两字节是 15 的 UTF-8 编码值。值 0x02 表示编码后的字符串值在 8 位块中的长度。

当消息编码后,标签和值会连接到一个字节流中。图 4-2 展示了如何在消息有多个字段的情况下,将字段值安排成字节流。流的结束会通过发送值为 0 的标签来进行标记。

现在,我们已经使用 protocol buffers 完成了对带有字符串字段的简单消息进行编码。protocol buffers 支持各种字段类型,有些字段类型有不同的编码机制。下面概述 protocol buffers 所使用的编码技术。

编码技术

protocol buffers 支持很多种编码技术,它会根据数据类型使用不同的编码技术。例如,字符串值会使用 UTF-8 字符编码,int32 则会使用名为 Varint 的技术进行编码。在设计消息定义时,了解各种数据类型对应的编码技术很重要,这样做能够为每个消息字段设置最合适的数据类型,从而让消息能够在运行时高效编码。

protocol buffers 所支持的字段类型被分成了不同的组,每组使用不同的技术来编码值。下面列出了 protocol buffers 中的几种常用的编码技术。

01. Varint 类型

Varint(可变长度整数)是使用单字节或多字节来序列化整数的方法。它基于这样一种思想:由于大多数数字并非均匀分布,因此为每个值所分配的字节数量不是固定的,而是依赖于具体的值。如表4-1 所示,像int32int64uint32uint64sint32sint64boolenum 这样的字段类型属于 Varint 类型,并且会按照 Varint 进行编码。表 4-2 展示了在 Varint 分类下的字段类型以及每个类型的用途。

表4-2:字段类型定义

字段类型 定义
int32 表示有符号整数的值类型,值的范围是–21474836482147483647。注意,这种类型在编码负数时效率较低
int64 表示有符号整数的值类型,值的范围是–92233720368547758089223372036854775807。注意,这种类型在编码负数时效率较低
uint32 表示无符号整数的值类型,值的范围是 04294967295
uint64 表示无符号整数的值类型,值的范围是 018446744073709551615
sint32 表示有符号整数的值类型,值的范围是–21474836482147483647。相比常规的 int32,这种类型能够更高效地编码负数(续)
sint64 表示有符号整数的值类型,值的范围是–92233720368547758089223372036854775807。相比常规的 int64,这种类型能够更高效地编码负数
bool 表示只有两种可能值的值类型,通常用于表示 truefalse
enum 表示一组命名值的值类型

在 Varint 中,除了最后的字节,其他所有字节都会设置最高有效位(most significant bit,MSB),表明后面还有字节。每字节中较低的 7 位用来存储数字的二进制补码形式。同时,最低有效组放在前面,这意味着我们要在低阶组中添加延续位。

02. 有符号整数类型

有符号整数是能够表示正整数值和负整数值的类型。像 sint32 和sint64 这样的字段类型就是有符号整数。对于有符号类型,会使用 zigzag 编码来将有符号整数转换成无符号整数。随后,无符号整数会使用前面的 Varint 编码技术来进行编码。

在 zigzag 编码中,有符号整数会将负整数和正整数以“之”字形的方式映射为无符号整数。表 4-3 展示了如何使用 zigzag 编码实现映射。

表4-3:针对有符号整数使用zigzag编码

原始值 映射值
0 0
–1 1
1 2
–2 3
2 4

如表 4-3 所示,映射值 0 依然对应原始值 0,其他值则按照“之”字形的方式匹配为正数。原始的负值匹配为奇数正值,原始的正值则匹配为偶数正值。通过 zigzag 编码后,不管原始值的符号是什么,得到的都是正数。在得到正数之后,就可以使用 Varint 对值进行编码。

对于负整数,推荐使用像 sint32 和 sint64 这样的有符号整数类型,这是因为如果使用像 int32 或 int64 这样的常规类型,就意味着使用 Varint 编码将负值转换成二进制值,但这比转换正值要使用更多的字节。因此,有效编码负数的方式就是将负数转换成正数,并对正数进行编码。在像 sint32 这样的有符号整数类型中,负数首先会使用 zigzag 编码转换成正数,然后再使用 Varint 进行编码。

03. 非 Varint 类型

非 Varint 类型恰好与 Varint 类型相反。它们分配固定数量的字节,字节数与实际值没有关系。protocol buffers 有两个线路类型属于非Varint 类型,其中一个用来表示 64 位的数据类型,如fixed64、sfixed64 和 double;另一个用来表示 32 位的数据类型,如 fixed32、sfixed32 和 float。

04. 字符串类型

在 protocol buffers 中,字符串类型属于基于长度分隔(lengthdelimited)的线路类型,这意味着首先会有一个经过 Varint 编码的长度值,随后才是指定数量的字节数据。字符串值会使用 UTF-8字符编码格式来进行编码。

以上就是编码常用数据类型所使用的技术。在 protocol buffers 的官网上,可以找到关于 protocol buffers 编码的详细介绍。

我们现在已经使用 protocol buffers 对消息进行了编码,接下来先将消息分帧,再通过网络将消息发送至服务器端。

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