Cap'n Proto
简介
Cap’n Proto 是非常快速的数据交换格式和基于容量的 RPC 系统, Cap'n Proto没有任何encoding/decoding步骤,Cap'n Proto编码的数据格式跟在内存里面的布局是一致的,所以可以直接将编码好的structure直接字节存放到硬盘上面。
Cap'n Proto的编码是方案是独立于任何平台的,但在现在的CPU上面(小端序)会有更高的性能。数据的组织类似compiler组织struct:固定宽度,固定偏移,以及合适的内存对齐,对于可变的数组使用pointer嵌入,而pointer也是使用的偏移存放而不是绝对地址。整数使用的是小端序,因为多数现代CPU都是小端序的。
其实如果熟悉C或者C++的结构体,就可以知道Cap'n Proto的编码方式就跟struct的内存布局差不多。
特性
增量读取
随机访问
mmap
内部语言通信:C++
Arena 分配
极小的生成代码
极小的运行时库
Time-traveling RPC
原理
Example
跟Protobuf一样,Cap'n Proto也需要定义描述文件,然后通过capnp的编译器编译成特定语言的对象使用。一个描述文件的简单例子:
|
|
几个需要关注的地方:
- 类型是定义在名字后面的,通常来说,对于一个变量来说,我们可能最关注的是它的名字,一个好的命名,就很容易让大家知道是干啥的。譬如上面的name一看就知道是表示的用户的名字。这点跟c语言是反的,它是先类型,在变量名,不过很多后续的语言,譬如go,rust等都是先名字,再类型了。
@N
用来给struct里面的field进行编号,编号从0开始,而且必须是连续的(这点跟Protobuf不一样)。上面birthdate虽然看起来在email和phones的前面,但是它的编号较大,实际编码的时候会放到后面。
参考
注释
使用 #
进行注释,注释应该跟在定义的后面,或者新启一行:
|
|
内置类型
原生支持的数据类型如下:
- Void:
Void
- Boolean:
Bool
- Integers:
Int8
,Int16
,Int32
,Int64
- Unsigned integers:
UInt8
,UInt16
,UInt32
,UInt64
- Floating-point:
Float32
,Float64
- Blobs:
Text
,Data
- Lists:
List(T)
需要注意:
Void
只有一个可能的值,使用0 bits进行编码,通常很少使用,但是可以作为union的member。Text
通常是UTF-8编码的,使用NULL结尾的字符串。Data
是任意二进制数据。List
是一个泛型类型,我们可以用特定类型去特化实现,譬如List(Int32)
就是一个Int32的List。
结构体
结构体其实类似于c的struct,field的有名字,有类型定义,同时需要编号:
|
|
Field也可以有默认值:
|
|
联合
Union是定义在struct里面同一个位置的一组fields,一次只能允许一个field被设置,我们使用不一样的tag来获知当前哪个field被设置了,不同于c里面的union,它不是类型,只是简单的fields聚合。
|
|
union可以没有名字,但是一个struct里面最多只能包含一个没名字的union:
|
|
对于union,我们需要注意:
- Union里面的field需要跟struct的field一起编号。
- 我们在上面的union中使用了
Void
类型,这个类型没有任何额外的信息,仅仅是为了跟其他状态区分。 - 通常,当一个struct初始化的时候,在union里面具有最小number field会被默认的设置,如果不想默认设置任何field,我们可以用在union里面的最小number定义一个unset的field。
- 我们可以将当前存在的field加入一个新的union,并且不会破坏当前数据的兼容性。
群组
我们通过group将一组fields封装到特定的作用域里面:
|
|
Group并不是struct里面独立的一个对象,它里面的fields仍然是struct的fields,需要跟其他struct的fields一起编号。
通常在一个struct里面使用group其实没啥大的意思,但是在union里面就比较有趣了:
|
|
在union里面使用group,我们很好的将field进行了自说明,现在看到radius,我们就知道它是circle的变量,而不需要额外的注释了。
当然,使用group,对于后续协议升级也是很有帮助的,在最开始的时候,我们的shape是square,但是现在想支持rectangle,如果需要额外的加入一个field。如果有group,我们仅仅需要添加一个新的group就可以了。
动态类型域
Struct可以定义field的类型为AnyPointer
,类似于c里面的void*
.
枚举
Enum就是一组符号值的集合:
|
|
Enum的成员必须从0开始编号,在c语言里面,enum通常都是数字类型的,但是在Cap'n Proto里面,它还可以是其他值。
接口
Interface是一组methods的集合,各个method可以有参数,有返回值,methods也必须从0开始编号。Interface支持继承,同样也支持多继承。
|
|
泛型
我们可以定义泛型的struct或者interface
|
|
在上面的例子中,我们定义了一个泛型的Map,然后在People里面用Text,Person作为参数来特化这个Map,如果我们了解c++的模板,就可以知道他们差不多。
泛型方法
interface也可以提供泛型method:
|
|
我们首先定义了一个泛型的interface,然后在对应的factory里面,创建这个interface的method就是泛型的method。
常量
我们可以用const来定义常量
|
|
我们可以直接引用这些常量
|
|
通常常量都都定义在全局scope里面,我们通过.
来进行引用获取。
嵌套,作用域以及别名
我们可以在struct或者interface里面嵌套常量,别名或者新的类型定义。
|
|
上面Baz里面我们通过Foo.Bar
来进行类型的获取。
我们可以使用using对一个类型设置别名。
|
|
导入
我们通过import导入其他文件的类型定义
|
|
也可以直接使用using来设置别名
|
|
或者这样
|
|
注解
有时候我们需要在Cap'n Proto上面附加一些不属于Cap'n Proto的自有协议。这就是Annotation,不过话说真有必要吗?这里还是先忽略吧。
唯一ID
每个Cap'n Proto文件都必须有唯一的一个64bit ID,使用capnp id
生成。譬如最开始例子里面的file ID
|
|
其实struct,enum这些的也需要定义ID,但默认情况下面,我们都是自动生成的。
64位的ID还是很可能冲突的,但是实际不用考虑这样的情况,反而是错误的使用(譬如copy了一个example但没有更改file ID)更可能导致冲突。
升级协议
如果我们要升级定义的协议,需要注意:
- 新的类型,常量或者别名可以添加到任何地方,他们不会影响现有的类型。
- 新的fields,enumerants以及methods需要使用比之前都要大的编号。
- 新加入到method里面的参数必须添加到参数列表的最后,并且有默认值。
- 成员可以随意在文件里面变换位置,只要number不变。
- 符号名字可以任意更改,只要ID和number别换就行了。但要注意默认生成的ID是根据父ID以及name来生成的,所以我们需要通过
capnp compile -ocapnp myschema.capnp
找到这个名字关联的ID并且在改名后显示的定义。 - 类型定义可以移动到任意的作用域,只要ID显示声明。
- 一个field可以被移入union或者group里面,就像在struct里面替换了以前的field,新加入了一个group或者union。
- 一个非泛型的类型可以变成泛型。(话说对于泛型的研究后续在考虑吧,总觉得没必要弄得这么复杂)
有一些操作是不安全的:
- 别更改field,method或者enum的number号。
- 别更改field,method参数的类型或者默认值。
- 别更改type的ID。
- 别随便更改没有显示ID的类型名字。
- 不能将没有显示ID的类型随便移到其他的作用域里面。
- 不能将一个已经存在的field移入/移除到一个已经存在的union里面。