协议编码
序列化
程序通常(至少)使用两种形式的数据:
-
在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中这些数据结构针对
CPU 的高效访问和操作进行了优化(通常使用指针) 。 -
如果要将数据写入文件,或通过网络发送,则必须将其编码(encode)为某种自包含的字节序列(例如,
JSON 文档)由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同。
序列化
-
编程语言特定的编码仅限于单一编程语言,并且往往无法提供前向和后向兼容性。
-
JSON,
XML 和CSV 等文本格式非常普遍,其兼容性取决于您如何使用它们。他们有可选的模式语言,这有时是有用的,有时是一个障碍。这些格式对于数据类型有些模糊,所以你必须小心数字和二进制字符串。 -
像
Thrift ,Protocol Buffers 和Avro 这样的二进制模式驱动格式允许使用清晰定义的前向和后向兼容性语义进行紧凑,高效的编码。这些模式可以用于静态类型语言的文档和代码生成。但是,他们有一个缺点,就是在数据可读之前需要对数据进行解码。
语言特定的格式
许多编程语言都内建了将内存对象编码为字节序列的支持。例如,
这些编码库非常方便,可以用很少的额外代码实现内存对象的保存与恢复。但是它们也有一些深层次的问题:
-
这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。如果以这类编码存储或传输数据,那你就和这门语言绑死在一起了。并且很难将系统与其他组织的系统(可能用的是不同的语言)进行集成。
-
为了恢复相同对象类型的数据,解码过程需要实例化任意类的能力,这通常是安全问题的一个来源:如果攻击者可以让应用程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执行任意代码。
-
在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进行编码,所以往往忽略了前向后向兼容性带来的麻烦问题。
-
效率(编码或解码所花费的
CPU 时间,以及编码结构的大小)往往也是事后才考虑的例如,Java 的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭着。
因此,除非临时使用,采用语言内置编码通常是一个坏主意。
JSON,XML 和二进制变体
谈到可以被许多编程语言编写和读取的标准化编码,
JSON,
- 数字的编码多有歧义之处。
XML 和CSV 不能区分数字和字符串(除非引用外部模式)JSON 虽然区分字符串和数字,但不区分整数和浮点数,而且不能指定精度。 - 当处理大量数据时,这个问题更严重了。例如,大于
$2^{53}$ 的整数不能在IEEE 754 双精度浮点数中精确表示,因此在使用浮点数(例如JavaScript )的语言进行分析时,这些数字会变得不准确Twitter 上有一个大于$2^{53}$ 的数字的例子,它使用一个64 位的数字来标识每条推文Twitter API 返回的JSON 包含了两种推特ID ,一个JSON 数字,另一个是十进制字符串,以此避免JavaScript 程序无法正确解析数字的问题。 JSON 和XML 对Unicode 字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(不带字符编码(character encoding) 的字节序列) 。二进制串是很实用的功能,所以人们通过使用Base64 将二进制数据编码为文本来绕开这个限制。模式然后用于表示该值应该被解释为Base64 编码。这个工作,但它有点hacky ,并增加了33 %的数据大小XML 和JSON 都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来相当复杂XML 模式的使用相当普遍,但许多基于JSON 的工具嫌麻烦才不会使用模式。由于数据的正确解释(例如数字和二进制字符串)取决于模式中的信息,因此不使用XML/JSON 模式的应用程序可能需要对相应的编码/ 解码逻辑进行硬编码。CSV 没有任何模式,因此应用程序需要定义每行和每列的含义。如果应用程序更改添加新的行或列,则必须手动处理该变更CSV 也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么? ) 。尽管其转义规则已经被正式指定,但并不是所有的解析器都正确的实现了标准。
尽管存在这些缺陷,但
模式编码
Protocol Buffers,
这些编码所基于的想法绝不是新的。例如,它们与
许多数据系统也为其数据实现某种专有的二进制编码。例如,大多数关系数据库都有一个网络协议,您可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库,并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序(例如使用
所以,我们可以看到,尽管
- 它们可以比各种“二进制
JSON ”变体更紧凑,因为它们可以省略编码数据中的字段名称。 - 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的(而手动维护的文档可能很容易偏离现实
) 。 - 保留模式数据库允许您在部署任何内容之前检查模式更改的向前和向后兼容性。
- 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查。
总而言之,模式进化允许与
Protocol Buffers 与Thrift
struct Person {
1: required string userName,
2: optional i64 favoriteNumber,
3: optional list<string> interests
}
message Person {
required string user_name = 1;
optional int64 favorite_number = 2;
repeated string interests = 3;
}
总体来说
模式演变
我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变
字段标签
从示例中可以看出,编码的记录就是其编码字段的拼接。每个字段由其标签号码(样本模式中的数字
您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。
向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的领域,你不能要求。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后 添加的每个字段必须是可选的或具有默认值。
删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除
删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除
数据类型
如何改变字段的数据类型?这可能是可能的——检查文件的细节——但是有一个风险,值将失去精度或被扼杀。例如,假设你将一个