學習 Cap'n proto

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;
}

幾個需要關注的地方:

參考

註釋

使用 #進行註釋,註釋應該跟在定義的後面,或者新啓一行:

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.
}

內置類型

原生支持的數據類型如下:

需要注意:

結構體

結構體其實類似於 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,我們需要注意:

羣組

我們通過 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)更可能導致衝突。

升級協議

如果我們要升級定義的協議,需要注意:

有一些操作是不安全的:

還有麼?

本文大多數只是對 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.
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://www.jianshu.com/p/f1110b22cb5c