标记语言与序列化
参考资料
标记语言
概念
- 描述一个对象, 本质上是对象 / 数组 / key-value 的嵌套结构
XML
<?xml version="1.0" encoding="UTF-8"?>
<!-- 第一行描述, 这是注释 -->
<message>
<warning>
Hello World
</warning>
</message>
YAML
languages:
- Ruby # 数组
- Perl
- Python
websites:
YAML: yaml.org # 对象
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org
# 纯量
boolean:
- TRUE # true, True 都可以
- FALSE # false, False 都可以
float:
- 3.14
- 6.8523015e+5 # 可以使用科学计数法
int:
- 123
- 0b1010_0111_0100_1010_1110 # 二进制表示
null:
nodeName: 'node'
parent: ~ # 使用~表示 null
string:
- 哈哈
- 'Hello world' # 可以使用双引号或者单引号包裹特殊字符
- newline
newline2 # 字符串可以拆成多行, 每一行会被转化成一个空格
date:
- 2018-02-17 # 日期必须使用 ISO 8601 格式, 即 yyyy-MM-dd
datetime:
- 2018-02-17T15:02:31+08:00 # 时间使用 ISO 8601 格式, 时间和日期之间使用 T 连接, 最后使用 + 代表时区
TOML
# 这是一个 TOML 文档
var_title = "TOML 示例" # 基本语法, 支持类型同上
[owner] # 表, 直至下一个表头
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [ 8000, 8001, 8002 ] # 数组
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 } # 内联表
[servers]
[servers.alpha]
ip = "10.0.0.1"
role = "前端"
[servers.beta]
ip = "10.0.0.2"
role = "后端"
INI
; Win 配置语言, 最简单的
[obj1] ;节
x=1
y=2
[obj2]
x=6
JSON
{}对象
[]数组
{
"sites": [
{ "name":"google", "url":"www.google.com" },
{ "name":"微博", "url":"www.weibo.com" }
]
}
序列化
- 将对象转换为字节序列的过程称为对象的序列化
- 将字节序列恢复为对象的过程称为对象的反序列化
- 其实就是约定的格式
- 常用于网络传输, 数据库存储, 进程间通信等场景
- 有些压缩方法省略结构信息, 所以需要声明 IDL 描述结构
JSON -> BSON
- MongoDB 用的, 别和 JSONB 混淆, JSONB 是可索引的表达抽象语法树的对象
- TLV (Tag-Length-Value) 的变体, 实际上更接近
Type-Key-Length-Value-0
- 先看 Type 决定类型 / 结构 (儿子, 同级, 数组, 数据 (数据类型))
- 再看 Key 决定字段名
- 存 Length 的目的是方便跳过
- 看真实数据
- 看见 0x00 (双重保险)
- JSON 只有
String, Number, Boolean, Array, Object, Null, BSON 增加了 Date (64位整数, UNIX 纪元毫秒数), BinData (二进制数据), ObjectId 等类型, 区分 Int32, Int64, Double, Decimal128 (高精度十进制)
- 不通用
FlatBuffers
- Google 开发, 最初为了游戏开发和对内存极其敏感的应用场景
- 零拷贝, 已编码数据与内存中的对象表示几乎一致, 读取时无需解析
- 偏移量表实现随机访问, 序列化时通常是从叶子节点向根节点构建 (倒序, 确定偏移量放在父的表里), 一旦生成, 很难原地修改数据
- Varint (变长整数): 使用 ZigZag 编码和 Base128 变长编码, 小整数只占 1 字节
- 紧凑排列: 所有字段紧挨着存放, 没有 padding
- 没那么通用 (不压缩是原罪)
- 需要定义 IDL 描述结构
Protobuf
- Google 的通用语言, 现代微服务架构的事实标准
- TLV, Key 被替换成了
field_number (方便增删), Tag = (field_number << 3) | wire_type(wire_type 为编码形式) 因此不传输字段名, 极大节省空间
- 如果 Tag 中类型为定长类型, 省略 Length
- Varints (变长整数) + ZigZag 编码 (解决 Varints 编码负数难题) + 去掉未赋值的字段等
- 需要定义
.proto 文件描述结构, 定义field_number, 事实标准
MessagePack
- 二进制 JSON
- TLV 将 JSON -> Key 的 TLV + Value 的 TLV
- TL 紧密排列
Thrift
- Facebook 开发, 后贡献给 Apache 通常指代整个 Thrift RPC 框架
- 双协议
TBinaryProtocol: 普通的二进制格式, 类似 Protobuf 但并未极致压缩
TCompactProtocol: 使用 Varint 和 ZigZag 压缩, 逻辑与 Protobuf 非常相似
- 更封闭的 Protobuf
Cap'n Proto
- MariaDB 之于 MySQL, Cap'n Proto 之于 Protobuf
- Protobuf 的核心思想是压缩与编码, 需要全量反序列化 + 重建对象
- Cap'n Proto 的核心思想是数据在内存中的布局 = 数据在传输线路上的布局, 无需反序列化, 直接访问内存 -> 实际场景中 Protobuf 在 CPU 上的反序列化成本较高
- 由于依赖固定的内存偏移量 (以此换取随机访问能力), 数据中会有大量的 Padding (填充零) 以对齐字节且通常不压缩整数
- 生成的不是持有数据的对象,而是访问器 (Readers/Builders)
- IDL 定义主结构体的全部信息, 变长数据用占位指针, 真实数据放在末尾 (实际策略更复杂)
- 自带 RPC 实现, 支持远程随机访问
- Promise Pipelining / 时间旅行 / 基于能力
- 客户端发送请求, 服务端未返回时, 就可以持有 Promise
- 基于定义的 Promise 类型能力, 可以继续调用
- 多个调用如果涉及多次请求, 可以一起发送