8个版本
使用旧的Rust 2015
0.3.1 | 2018年5月18日 |
---|---|
0.3.0 | 2017年2月10日 |
0.2.1 | 2016年10月10日 |
0.2.0 | 2016年4月1日 |
0.1.2 | 2015年8月11日 |
#1536 在 编码
每月 31 次下载
50KB
1.5K SLoC
Probor
状态 | 概念验证 |
---|---|
Rust 文档 | http://tailhook.github.io/probor/ |
Probor 是一个在CBOR之上扩展的结构化数据序列化机制。
除了CBOR之外,Probor还具有以下功能
- 一个库,可以高效地将数据读取到语言原生结构中
- 一个模式定义语言,作为系统间互操作性的文档
- 一个更紧凑的协议,省略了对象字段名
- 约定,使模式向后兼容
为什么?
我们喜欢CBOR的原因如下
- 它是IETF标准
- 它是自描述的
- 它足够紧凑
- 它非常丰富,包括令人难以置信的事物
- 它在大多数语言中都有实现
我们在CBOR中缺少的
- 没有模式定义,即无法检查系统间的互操作性
- 传输/存储大量对象很昂贵,因为每个对象都会对键进行编码
- 没有将“对象”(即映射或字典)转换为原生类型对象的标准化方法
比较
本节大致比较了类似的项目,以了解“为什么?”部分中的论点。单个论点可能不是非常有说服力,但总体上是合理的。
Probor 与 Protobuf 的比较
Protobuf 如果没有模式就无法解析数据。Probor 并非总是完全可读,但至少可以使用通用的 cbor 解码器解包数据并查看原始值(可能没有键名)。
并且不仅在没有模式时困难,当有模式但没有生成代码来检查时也困难。例如,如果你有一个Java应用程序,但想检查一些Python代码。你需要一个Pythonic代码生成器并在生成代码之前才能使用 protobuf 读取任何内容。
Probor 还具有调试(非压缩)模式,其中它可以通过名称对对象和枚举进行编码,以便您可以轻松理解值。您还可以保留大多数对象的关键名称,除了大量传输的对象,因为压缩和非压缩格式是兼容的。您是掌控者。
Protobuf 生成的类型不是原生的。因此,它们更大且难以处理。由于代码是生成的,通常您不能在对象本身上添加方法而不进行细微的篡改。Probor 尝试在原生对象周围提供一层薄薄的保护。
与代码生成一起工作也不方便。Protobuf 有一个用 C++ 编写的代码生成器,您需要安装它。此外,您通常需要为每种语言安装不同版本的 protobuf 代码生成器。Probor 通过提供简单易用的宏和/或注解来原生类型,无需代码生成即可在所有当前支持的语言中工作。我们也可以提供代码生成功能来启动代码,但这些应该完全用它们生成的语言来完成。
在 Protobuf 的正面,它可以不丢失任何信息(甚至是他协议版本中不存在的字段)地反序列化查找对象并再次序列化。对于 probor,由于效率原因,当前库中没有实现,但仍然可以使用适当的库来完成。
Probor 与 Avro 的比较
Avro 需要一个模式来在数据发送前传输“带内”,即作为数据发送的前缀。我们认为这是多余的。
此外,Avro 类型在一定程度上受到了 C 时代的限制。我们希望使用像 Rust 或 Haskell 中那样的现代代数类型。
Probor 与 Thrift 的比较
Thrift 没有很好地描述二进制格式(实际上有两个,都没有以任何合理的方式记录),与 IETF 标准的 CBOR 不同,这使得在没有提前生成代码的情况下阅读数据变得困难。
Thrift 还有一个丑陋的联合类型,从 1990x 时代开始,与我们在 2015 年想要使用的漂亮代数类型相比。
Thrift 依赖于代码生成来解析数据,我们不喜欢这样做,因为它使得程序难以构建,并且难以与原生类型集成(即向生成类型添加方法)。
此外,thrift 绑定通常有一些服务的实现,这通常是冗余的,因为每种语言处理网络的方式太多了,以至于不能由 thrift 作者实现所有这些。此外,thrift 有一个长期历史,生成不能与网络 IO 无关的代码。
Probor 与 Capnproto 的比较
Capnproto 有一个丑陋且复杂的序列化格式,它可以将值直接映射到内存中而不进行解码。但它的实现比我们目标更复杂。我们还希望有紧凑的编码,这是 Capnproto 拥有的,但它是建立在已经难以理解的编码之上的,使事情更加复杂。
像其他一样,Capnproto 也依赖于代码生成,解码后的结果是丑陋的协议对象,但我们希望使用原生类型。
类似物
例如,这里有一个模式
struct SearchResults { total_results @0 :int results @1 :array Page } struct Page { url @0 :text, title @1 :text, snippet @2 :optional text, }
注意以下事项
- 我们使用泛型类型名称,如 int(整数),而不是固定宽度(参见常见问题解答)
- 我们给每个字段一个编号,它们与用于其他 IDL(如 protobuf、thrift 或 capnproto)的编号类似
使用 probor 序列化的结构将看起来像(以 JSON 显示清晰度,实际上如果您解码 CBOR 并用 JSON 编码,您将看到确切的数据)
[1100, [ ["http://example.com", "Example Com"], ["http://example.org", "Example Org", "Example organization"]]]
显然,当解包时,它看起来更像是(在 JavaScript 中)
new SearchResults({"total_results": 1100, "results": [new Page({"url": "http://example.com", "title": "Example Com"}), new Page({"url": "http://example.org", "title": "Example Org", "snippet": "Example organization"})]}
实际上,对象可以像这样序列化
{"total_results": 1100, "results": [{"url": "http://example.com", "title": "Example Com"}, {"url": "http://example.org", "title": "Example Org", "snippet": "Example organization"}]}
这也会是一个完全有效的序列化表示。也就是说,您可以通过名称和数字存储字段。这在处理临时请求或您愿意从前端接收非紧凑数据,然后以更紧凑的格式验证和推送数据进行存储时,偶尔会很有用。
在Python中序列化看起来像这样
from probor import struct class Page(object): def __init__(self, url, title, snippet=None): # .. your constructor .. omitted for brevity probor_protocol = struct( required={(0, "url"): str, (1, "title"): str}, optional={(2, "snippet"): str}) class SearchResults(object): def __init__(self, total_resutls, results): # .. your constructor .. omitted for brevity probor_protocol = struct( required={(0, "total_results"): int, (1, "results"): Page})
待办事项:语法是否丑陋?应该更强制吗?使用了setstate/getstate吗?
注意
在这个协议之上构建一个更声明式的层很容易。例如,对于某些ORM模型,您可能可以重用字段名称和类型。但需要记住的重要属性是,您不应该依赖字段顺序来编号字段,并且数字必须是显式的,否则删除字段可能会被忽略。
除此之外,鼓励将probor数据类型与模型和/或验证代码集成。这也是我们不为此底层声明提供更简洁语法的实际原因。
类似地,在Rust中它看起来像这样
#[macro_use] extern crate probor; use probor::{Encoder, Encodable}; use probor::{Decoder, Config, decode}; use std::io::Cursor; probor_struct!( #[derive(PartialEq, Eq, Debug)] struct Page { url: String => (#0), title: String => (#1), snippet: Option<String> => (#2 optional), }); probor_struct!( #[derive(PartialEq, Eq, Debug)] struct SearchResults { total_results: u64 => (#0), results: Vec<Page> => (#1), }); fn main() { let buf = Vec::new(); let mut enc = Encoder::new(buf); SearchResults { total_results: 112, results: vec![Page { url: "http://url1.example.com".to_string(), title: "One example".to_string(), snippet: None, }, Page { url: "http://url2.example.com".to_string(), title: "Two example".to_string(), snippet: Some("Example Two".to_string()), }], }.encode(&mut enc).unwrap(); let sr: SearchResults = decode( &mut Decoder::new(Config::default(), Cursor::new(enc.into_writer()))) .unwrap(); println!("Results {:?}", sr); }
Rust的例子略长,但对于Rust来说是可以忍受的。它很大程度上基于宏,这可能看起来与代码生成相似。然而,我们发现它更好,因为您至少可以控制以下事物
- 使用的特定类型(例如,u64用于int)
- 结构定义(可以使用包括derive和repr在内的元属性,并可以使用struct T(X, Y))
- 对象是如何创建的(例如,使用VecDeque或BTreeMap而不是默认的Vec和HashMap)
- 如何处理缺失的字段(例如,您可以为缺失的字段提供默认值,而不是使用Option<T>)
- 您还可以包含特定于应用程序的验证代码
注意
将括号留空会导致字段字符串作为有效负载的一部分存储。这将违背减少存储数据字节量的目标,在这种情况下,直接使用CBOR可能更好。
最终,使用少量辅助宏显式编写解析器看起来比将所有数据作为元信息添加到模式文件中要好得多。
类型系统
结构
待定
代数类型
待定
在不支持的语言中
在不支持代数类型的语言中,它们通过连接几个普通类型来实现。例如,以下Rust中的类型
enum HtmlElement { Tag(String, Vec<HtmlElement>), Text(String), }
在Python中编码如下
from probor import enum class HtmlElement: """Base class""" class Tag(HtmlElement): def __init__(self, tag_name, children): # .. snip .. probor_protocol = ... class Text(HtmlElement): def __init__(self, text) self.text = text probor_protocol = ... HtmlElement.probor_protocol = enum({ (0, 'Tag'): Tag, (1, 'Text'): Text, })
然后您可以通过使用functools.singledispatch(在Python3.4中)或只是使用isinstance.
注意
PureScript编译类型的方式类似。它是未检查的,但我相信probor的序列化到JavaScript应该与PureScript类型兼容。
前向/后向兼容性
与protobuf相比,probor序列化器始终将所有字段视为可选的。必需字段仅在IDL中,因此如果您的未来类型足够智能,可以
后向兼容性与protobuf非常相似。
待定:后向兼容性的确切规则
待定:前向兼容性的确切规则
待定:将结构转换为具有兼容性的代数类型
常见问题解答
为什么使用泛型类型?
好吧,有几个原因
- 不同的语言有不同的类型,例如,Python只有整数,Java没有无符号整数类型
- 固定宽度的类型根本不是很好的约束,有效的值往往比类型的范围小得多,所以这根本不是数据验证的替代品
为什么没有默认值?
有几个原因
- 默认值是用户界面功能。每个服务都可能希望使用它自己的默认值。
- 当序列化时,值等于默认值时是否可以省略,这非常特定于应用程序。我们希望使用不带任何额外记录的语言的本地结构,以确定值是否是默认值或只是等于它。
依赖关系
~0.2–0.8MB
~16K SLoC