Skip to content

Commit

Permalink
修复第 4 章有歧义的内容 (#134)
Browse files Browse the repository at this point in the history
* 修复第 4 章有歧义的内容
  • Loading branch information
fuxuemingzhu authored Oct 11, 2021
1 parent 96dd3f6 commit 86b979f
Showing 1 changed file with 9 additions and 9 deletions.
18 changes: 9 additions & 9 deletions ch4.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Thrift和Protocol Buffers每一个都带有一个代码生成工具,它采用

[图4-1](img/fig4-1.png)相比,最大的区别是没有字段名`(userName, favoriteNumber, interests)`。相反,编码数据包含字段标签,它们是数字`(1, 2和3)`。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。

Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如[图4-3](img/fig4-3.png)所示,它只将相同的信息打包成只有34个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字1337不是使用全部八个字节,而是用两个字节编码,每个字节的最高位用来指示是否还有更多的字节来。这意味着-64到63之间的数字被编码为一个字节,-8192和8191之间的数字以两个字节编码,等等。较大的数字使用更多的字节。
Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如[图4-3](img/fig4-3.png)所示,它只将相同的信息打包成只有34个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字1337不是使用全部八个字节,而是用两个字节编码,每个字节的最高位用来指示是否还有更多的字节。这意味着-64到63之间的数字被编码为一个字节,-8192和8191之间的数字以两个字节编码,等等。较大的数字使用更多的字节。

![](img/fig4-3.png)

Expand All @@ -166,15 +166,15 @@ Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如[图4

**图4-4 使用Protobuf编码的记录**

需要注意的一个细节:在前面所示的模式中,每个字段被标记为必需或可选,但是这对字段如何编码没有任何影响(二进制数据中没有任何字段指示是否需要字段)。所不同的是,如果未设置该字段,则所需的运行时检查将失败,这对于捕获错误非常有用。
需要注意的一个细节:在前面所示的模式中,每个字段被标记为必需或可选,但是这对字段如何编码没有任何影响(二进制数据中没有任何字段指示某字段是否必须)。区别在于,如果字段设置为 `required`,但未设置该字段,则所需的运行时检查将失败,这对于捕获错误非常有用。

#### 字段标签和模式演变

我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变。 Thrift和Protocol Buffers如何处理模式更改,同时保持向后兼容性?

从示例中可以看出,编码的记录就是其编码字段的拼接。每个字段由其标签号码(样本模式中的数字1,2,3)标识,并用数据类型(例如字符串或整数)注释。如果没有设置字段值,则简单地从编码记录中省略。从中可以看到,字段标记对编码数据的含义至关重要。您可以更改架构中字段的名称,因为编码的数据永远不会引用字段名称,但不能更改字段的标记,因为这会使所有现有的编码数据无效。

您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。
您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了向前兼容性:旧代码可以读取由新代码编写的记录。

向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的字段,你不能设置为必需。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后 **添加的每个字段必须是可选的或具有默认值**

Expand All @@ -184,13 +184,13 @@ Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如[图4

如何改变字段的数据类型?这也许是可能的——详细信息请查阅相关的文档——但是有一个风险,值将失去精度或被截断。例如,假设你将一个32位的整数变成一个64位的整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位,则它将被截断。

Protobuf的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(这是除必需和可选之外的第三个选项)。如[图4-4](img/fig4-4.png)所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
Protobuf的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(`repeated`这是除必需和可选之外的第三个选项)。如[图4-4](img/fig4-4.png)所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。

Thrift有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的演变,但是它具有支持嵌套列表的优点。

### Avro

Apache Avro 【20】是另一种二进制编码格式,与Protocol Buffers和Thrift有趣的不同。 它是作为Hadoop的一个子项目在2009年开始的,因为Thrift不适合Hadoop的用例【21】。
Apache Avro 【20】是另一种二进制编码格式,与Protocol Buffers和Thrift有着有趣的不同。 它是作为Hadoop的一个子项目在2009年开始的,因为Thrift不适合Hadoop的用例【21】。

Avro也使用模式来指定正在编码的数据的结构。 它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于JSON)更易于机器读取。

Expand Down Expand Up @@ -292,7 +292,7 @@ Avro的关键思想是Writer模式和Reader模式不必是相同的 - 他们只

Thrift和Protobuf依赖于代码生成:在定义了模式之后,可以使用您选择的编程语言生成实现此模式的代码。这在Java,C ++或C#等静态类型语言中很有用,因为它允许将高效的内存中结构用于解码的数据,并且在编写访问数据结构的程序时允许在IDE中进行类型检查和自动完成。

在动态类型编程语言(如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了显示的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的Avro模式),代码生成对获取数据是一个不必要的障碍。
在动态类型编程语言(如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了显式的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的Avro模式),代码生成对获取数据是一个不必要的障碍。

Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件(它嵌入了Writer模式),你可以简单地使用Avro库打开它,并以与查看JSON文件相同的方式查看数据。该文件是自描述的,因为它包含所有必要的元数据。

Expand Down Expand Up @@ -425,7 +425,7 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:好的时候它可能会在不到一毫秒的时间内完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完成一样的东西。
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。

客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 —— 例如回想一下JavaScript的数字大于$2^{53}$的问题(请参阅“[JSON,XML和二进制变体](#JSON,XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
- 客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 —— 例如回想一下JavaScript的数字大于$2^{53}$的问题(请参阅“[JSON,XML和二进制变体](#JSON,XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。

所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在REST之上构建RPC库)。

Expand Down Expand Up @@ -479,13 +479,13 @@ RPC方案的前后向兼容性属性从它使用的编码方式中继承:

一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在[第十一章](ch11.md)中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流,类似于RPC)。

消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后和向前兼容的,您可以灵活地对发布者和消费者的编码进行独立的修改,并以任意顺序进行部署。
消息代理通常不会执行任何特定的数据模型 —— 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后和向前兼容的,您可以灵活地对发布者和消费者的编码进行独立的修改,并以任意顺序进行部署。

如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图4-7](img/fig4-7.png))。

#### 分布式的Actor框架

Actor模型是单个进程中并发的编程模型。逻辑被封装在actor中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)。每个actor通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。消息传送不保证:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。
Actor模型是单个进程中并发的编程模型。逻辑被封装在actor中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)。每个actor通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。不保证消息传送:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。

在分布式Actor框架中,此编程模型用于跨多个节点伸缩应用程序。不管发送方和接收方是在同一个节点上还是在不同的节点上,都使用相同的消息传递机制。如果它们在不同的节点上,则该消息被透明地编码成字节序列,通过网络发送,并在另一侧解码。

Expand Down

0 comments on commit 86b979f

Please sign in to comment.