美文网首页
学习Cap'n proto

学习Cap'n proto

作者: siddontang | 来源:发表于2016-01-06 14:58 被阅读3697次

    Cap'n Proto

    Introduction

    最近在研究一些编码的事情,然后就发现了Cap'n Proto,作者号称性能是直接秒杀Google Protobuf,直接上官方对比:

    性能对比性能对比

    虽然我知道很多编码方案都比Google Protobuf要快很多,但性能快到这个地步,还是第一次听说,加之后续我们决定自己的系统也是用这套编码方案,于是就需要好好研究一下了。

    为啥这么快,Cap'n Proto的文档里面就立刻说明了,因为这个测试Cap'n Proto没有任何encoding/decoding步骤,Cap'n Proto编码的数据格式跟在内存里面的布局是一致的,所以可以直接将编码好的structure直接字节存放到硬盘上面。

    Cap'n Proto的编码是方案是独立于任何平台的,但在现在的CPU上面(小端序)会有更高的性能。数据的组织类似compiler组织struct:固定宽度,固定偏移,以及合适的内存对齐,对于可变的数组使用pointer嵌入,而pointer也是使用的偏移存放而不是绝对地址。整数使用的是小端序,因为多数现代CPU都是小端序的。

    其实如果熟悉C或者C++的结构体,就可以知道Cap'n Proto的编码方式就跟struct的内存布局差不多。

    Example

    跟Protobuf一样,Cap'n Proto也需要定义描述文件,然后通过capnp的编译器编译成特定语言的对象使用。一个描述文件的简单例子:

    @0xdbb9ad1f14bf0b36;  # unique file ID, generated by `capnp id`
    
    struct Person {
      name @0 :Text;
      birthdate @3 :Date;
    
      email @1 :Text;
      phones @2 :List(PhoneNumber);
    
      struct PhoneNumber {
        number @0 :Text;
        type @1 :Type;
    
        enum Type {
          mobile @0;
          home @1;
          work @2;
        }
      }
    }
    
    struct Date {
      year @0 :Int16;
      month @1 :UInt8;
      day @2 :UInt8;
    }
    

    几个需要关注的地方:

    • 类型是定义在名字后面的,通常来说,对于一个变量来说,我们可能最关注的是它的名字,一个好的命名,就很容易让大家知道是干啥的。譬如上面的name一看就知道是表示的用户的名字。这点跟c语言是反的,它是先类型,在变量名,不过很多后续的语言,譬如go,rust等都是先名字,再类型了。
    • @N用来给struct里面的field进行编号,编号从0开始,而且必须是连续的(这点跟Protobuf不一样)。上面birthdate虽然看起来在email和phones的前面,但是它的编号较大,实际编码的时候会放到后面。

    参考

    注释

    使用 #进行注释,注释应该跟在定义的后面,或者新启一行:

    struct Date {
      # A standard Gregorian calendar date.
    
      year @0 :Int16;
      # The year.  Must include the century.
      # Negative value indicates BC.
    
      month @1 :UInt8;   # Month number, 1-12.
      day @2 :UInt8;     # Day number, 1-30.
    }
    

    内置类型

    原生支持的数据类型如下:

    • 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的有名字,有类型定义,同时需要编号:

    struct Person {
      name @0 :Text;
      email @1 :Text;
    }
    

    Field也可以有默认值:

    foo @0 :Int32 = 123;
    bar @1 :Text = "blah";
    baz @2 :List(Bool) = [ true, false, false, true ];
    qux @3 :Person = (name = "Bob", email = "bob@example.com");
    corge @4 :Void = void;
    grault @5 :Data = 0x"a1 40 33";
    

    联合

    Union是定义在struct里面同一个位置的一组fields,一次只能允许一个field被设置,我们使用不一样的tag来获知当前哪个field被设置了,不同于c里面的union,它不是类型,只是简单的fields聚合。

    struct Person {
      # ...
    
      employment :union {
        unemployed @4 :Void;
        employer @5 :Company;
        school @6 :School;
        selfEmployed @7 :Void;
        # We assume that a person is only one of these.
      }
    }
    

    union可以没有名字,但是一个struct里面最多只能包含一个没名字的union:

    struct Shape {
      area @0 :Float64;
    
      union {
        circle @1 :Float64;      # radius
        square @2 :Float64;      # width
      }
    }
    

    对于union,我们需要注意:

    • Union里面的field需要跟struct的field一起编号。
    • 我们在上面的union中使用了Void类型,这个类型没有任何额外的信息,仅仅是为了跟其他状态区分。
    • 通常,当一个struct初始化的时候,在union里面具有最小number field会被默认的设置,如果不想默认设置任何field,我们可以用在union里面的最小number定义一个unset的field。
    • 我们可以将当前存在的field加入一个新的union,并且不会破坏当前数据的兼容性。

    群组

    我们通过group将一组fields封装到特定的作用域里面:

    struct Person {
      # ...
    
      # Note:  This is a terrible way to use groups, and meant
      #   only to demonstrate the syntax.
      address :group {
        houseNumber @8 :UInt32;
        street @9 :Text;
        city @10 :Text;
        country @11 :Text;
      }
    }
    

    Group并不是struct里面独立的一个对象,它里面的fields仍然是struct的fields,需要跟其他struct的fields一起编号。

    通常在一个struct里面使用group其实没啥大的意思,但是在union里面就比较有趣了:

    struct Shape {
      area @0 :Float64;
    
      union {
        circle :group {
          radius @1 :Float64;
        }
        rectangle :group {
          width @2 :Float64;
          height @3 :Float64;
        }
      }
    }
    

    在union里面使用group,我们很好的将field进行了自说明,现在看到radius,我们就知道它是circle的变量,而不需要额外的注释了。

    当然,使用group,对于后续协议升级也是很有帮助的,在最开始的时候,我们的shape是square,但是现在想支持rectangle,如果需要额外的加入一个field。如果有group,我们仅仅需要添加一个新的group就可以了。

    动态类型域

    Struct可以定义field的类型为AnyPointer,类似于c里面的void*.

    枚举

    Enum就是一组符号值的集合:

    enum Rfc3092Variable {
      foo @0;
      bar @1;
      baz @2;
      qux @3;
      # ...
    }
    

    Enum的成员必须从0开始编号,在c语言里面,enum通常都是数字类型的,但是在Cap'n Proto里面,它还可以是其他值。

    接口

    Interface是一组methods的集合,各个method可以有参数,有返回值,methods也必须从0开始编号。Interface支持继承,同样也支持多继承。

    interface Node {
      isDirectory @0 () -> (result :Bool);
    }
    
    interface Directory extends(Node) {
      list @0 () -> (list: List(Entry));
      struct Entry {
        name @0 :Text;
        node @1 :Node;
      }
    
      create @1 (name :Text) -> (file :File);
      mkdir @2 (name :Text) -> (directory :Directory);
      open @3 (name :Text) -> (node :Node);
      delete @4 (name :Text);
      link @5 (name :Text, node :Node);
    }
    
    interface File extends(Node) {
      size @0 () -> (size: UInt64);
      read @1 (startAt :UInt64 = 0, amount :UInt64 = 0xffffffffffffffff)
           -> (data: Data);
      # Default params = read entire file.
    
      write @2 (startAt :UInt64, data :Data);
      truncate @3 (size :UInt64);
    }
    

    泛型

    我们可以定义泛型的struct或者interface

    struct Map(Key, Value) {
      entries @0 :List(Entry);
      struct Entry {
        key @0 :Key;
        value @1 :Value;
      }
    }
    
    struct People {
      byName @0 :Map(Text, Person);
      # Maps names to Person instances.
    }
    

    在上面的例子中,我们定义了一个泛型的Map,然后在People里面用Text,Person作为参数来特化这个Map,如果我们了解c++的模板,就可以知道他们差不多。

    泛型方法

    interface也可以提供泛型method:

    interface Assignable(T) {
      # A generic interface, with non-generic methods.
      get @0 () -> (value :T);
      set @1 (value :T) -> ();
    }
    
    interface AssignableFactory {
      newAssignable @0 [T] (initialValue :T)
          -> (assignable :Assignable(T));
      # A generic method.
    }
    

    我们首先定义了一个泛型的interface,然后在对应的factory里面,创建这个interface的method就是泛型的method。

    常量

    我们可以用const来定义常量

    const pi :Float32 = 3.14159;
    const bob :Person = (name = "Bob", email = "bob@example.com");
    const secret :Data = 0x"9f98739c2b53835e 6720a00907abd42f";
    

    我们可以直接引用这些常量

    const foo :Int32 = 123;
    const bar :Text = "Hello";
    const baz :SomeStruct = (id = .foo, message = .bar);
    

    通常常量都都定义在全局scope里面,我们通过.来进行引用获取。

    嵌套,作用域以及别名

    我们可以在struct或者interface里面嵌套常量,别名或者新的类型定义。

    struct Foo {
      struct Bar {
        #...
      }
      bar @0 :Bar;
    }
    
    struct Baz {
      bar @0 :Foo.Bar;
    }
    

    上面Baz里面我们通过Foo.Bar来进行类型的获取。

    我们可以使用using对一个类型设置别名。

    struct Qux {
      using Foo.Bar;
      bar @0 :Bar;
    }
    
    struct Corge {
      using T = Foo.Bar;
      bar @0 :T;
    }
    

    导入

    我们通过import导入其他文件的类型定义

    struct Foo {
      # Use type "Baz" defined in bar.capnp.
      baz @0 :import "bar.capnp".Baz;
    }
    

    也可以直接使用using来设置别名

    using Bar = import "bar.capnp";
    
    struct Foo {
      # Use type "Baz" defined in bar.capnp.
      baz @0 :Bar.Baz;
    }
    

    或者这样

    using import "bar.capnp".Baz;
    
    struct Foo {
      baz @0 :Baz;
    }
    

    注解

    有时候我们需要在Cap'n Proto上面附加一些不属于Cap'n Proto的自有协议。这就是Annotation,不过话说真有必要吗?这里还是先忽略吧。

    唯一ID

    每个Cap'n Proto文件都必须有唯一的一个64bit ID,使用capnp id生成。譬如最开始例子里面的file ID

    # file ID
    @0xdbb9ad1f14bf0b36;
    

    其实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里面。

    还有么?

    本文大多数只是对Cap'n Proto的文档进行了自己的中文解释,仅仅列出了Cap'n Proto的语言参考,没有涉及到RPC的问题。另外,因为我们的系统不是C++开发,所以还需要熟悉特定语言下面Cap'n Proto的使用。这些都会后续会好好研究。

    最后,Cap'n Proto的LICNESE:

    Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
    Licensed under the MIT License:
    
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.
    

    相关文章

      网友评论

          本文标题:学习Cap'n proto

          本文链接:https://www.haomeiwen.com/subject/civxkttx.html