Google Prorobuf And Node addones
发布于 2 年前 作者 Acceptedlc 2022 次浏览 来自 分享

Protocol Buffer 大业

<h2 id=“1”>First Simple Message</h2>

syntax = "proto3";

message Test1
{
    uint32 id = 1;
}
  • proto3 说明使的是version 3的语法
  • Test1 是Message的名字,与class对应
  • 这个message生命了一个field,是int32类型的
  • 最后的1是这个字段的标签,在二进制中用这个数字代表这个字段

<h3 id=“1.1”> Message编译后,使用方法如下 </h3>

const TestProro = require("../out/test_pb.js");
let test1 = new TestProro.Test1();
test1.setId(980);
console.log(test1.serializeBinary());

out:
Uint8Array [ 8, 212, 7 ]

输出分析

十进制 二进制
8 0000 1000
212 1101 0100
7 0000 0111

<h3 id=“1.2”> 消息结构 </h3>

protobuf的消息是经过编码序列化的一系列key-value对,一个类型的数据对应一个key-value。value就是原始数据经过编码后的数据,而key由field number和wire type组成

(field_number << 3) | wire_type

wire type

Type 序列化后 序列化前
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64 fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

上例中field_number = 1,wire_type = 0。

  • field_number << 3 -----> 1000 (二进制)
  • 与wire_type 做与运算 -------> 1000 (二进制)
  • 因此,这个byte是 0000 1000cn

<h3 id=“1.3”> Varint </h3>

value 是一个无符号整形,pb使用Varint(可变长度int)进行编码。

Varint第一位是一个标志位,表示下一个字节与本字节是否是一个整体(1代表是,0代表不是)。

先来看一个简单的“1”,用一个字节足以表示,因此,符号位为零—— 0000 0001

对例子中的980,采用1个字节不足以表示。 将标志位去掉得到:1010100 0000111

pb采用小端模式,最后的value位:0000111 1010100 = 980

Varint的效果还是很明显的,对于一个字长位32bit的计算机,数据压缩了50%

<h2 id=“2”> 有符号整形 </h2>

<h3 id=“2.1”> 有符号整形在计算机中的存储方案 </h3>

以32位字长的计算机为例子

  • 一个整形数会占据32bit,最高的一个bit表示符号
    • 0代表正数
    • 1代表负数
  • 计算机中的数字采用补码存储
    • 正数的补码就是其本身
    • 负数的补码除符号位外,对源码剩余bit依次取反后加一

以-1为例

源码: 1000 0000 0000 0000 0000 0000 0000 0001
反码: 1111 1111 1111 1111 1111 1111 1111 1110
补码: 1111 1111 1111 1111 1111 1111 1111 1111

可以看到,如果直接传输-1,需要传输32bit。protocol buffer认为这样很浪费,所以对于负数先采用Zigzag编码,对编码后的内容使用Varint压缩

<h3 id=“2.2”> Zigzag编码 </h3>

以32位字长的计算机为例子

Zigzag算法实际上可以理解为一个hash函数,定义如下:

hash(n) = (n << 1) ^ (n >> 31)

Tips:

  • 左移
    • 舍弃左边的一位
    • 右边空出的位用0填补
  • 右移
    • 舍弃右边的一位
    • 左边
      • 正数:用0补位
      • 右边:用1补位

看一个例子:

message Test2 {
  sint32 id = 1;
}

let test2 = new TestProro.Test2();
test2.setId(1);
console.log(test2.serializeBinary());

OutPut:
Uint8Array [ 8, 2 ]

n: 0000 0000 0000 0000 0000 0000 0000 0001
n<< 1 : 0000 0000 0000 0000 0000 0000 0000 0010
n>>31: 0000 0000 0000 0000 0000 0000 0000 0000
最终结果: 0000 0000 0000 0000 0000 0000 0000 0010
结果的十进制: 2

test2 = new TestProro.Test2();
test2.setId(-1);
console.log(test2.serializeBinary());

OutPut:
Uint8Array [ 8, 1 ]

n: 1111 1111 1111 1111 1111 1111 1111 1111
n<< 1 : 1111 1111 1111 1111 1111 1111 1111 1110
n>>31: 1111 1111 1111 1111 1111 1111 1111 1111
最终结果: 0000 0000 0000 0000 0000 0000 0000 0001
结果的十进制: 1

实际上,Zigzag的hash函数想要达到的效果是:

  • 正数: n * 2
  • 负数: n * 2 - 1

换句刷说,就是讲有符号正数映射为无符号正数,然后再使用Varint编码

从效果上来说,对于-1,本来需要传递32个bit,现在只需要传递8bit,节省了75%的开销

<h2 id=“4”>字符串的编码</h2>

message Test3
{
  string str = 2;
}

console.log();
console.log("字符串的编码")
let test3 = new TestProro.Test3();
test3.setStr("aaa");
console.log(test3.serializeBinary());

OutPut:
Uint8Array [ 18, 3, 97, 97, 97 ]

field = 2 wire type = 2

所以消息的key位 100010 = 18

对于字符串,第二个byte表示这个string的length,因此序列化后的第二个byte位3,剩下的3个byte则是a的asc码

<h2 id=“5”>嵌套Message</h2>

message Test4
{
  Test1 c = 2;
}

console.log();
console.log("嵌套Message")
let test4 = new TestProro.Test4();
test4.setC(test1);
console.log(test4.serializeBinary());

OutPut:
Uint8Array [ 18, 3, 8, 212, 7 ]

  • 在父message中wire type=2
  • value则是,子message序列化后的内容

<h2 id=“6”>使用PB,扩展js</h2>

在我目前的项目中,考虑使用pb的最大的原因是用c++解析json,开发效率有些低,而pb可以直接将数据pass成相应的类实例,使用起来也很方便。

随着,spark的引入,在java当中,也会解析一遍json,会让这段逻辑再写一遍

在js addones当中,只要将编译好的内容,放到一个Buffer当中。在c++中就可以用Node提供的Buffer API 获得一个byte数组,使用这个数组可以直接得到一个c++的实例

  • node::Buffer::Data
  • node::Buffer::Length
let bytes = action.serializeBinary();
let msg = [Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes),Buffer.from(bytes)]
console.time("proto");
addones.proto(msg);
console.timeEnd("proto");

NAN_METHOD(Proro) {

  v8::Local<v8::Array> input = v8::Local<v8::Array>::Cast(info[0]);
  unsigned int num_locations = input->Length();
  for (unsigned int i = 0; i < num_locations; i++) {
    char* buffer =  (char*) node::Buffer::Data(input->Get(i));
    int length = node::Buffer::Length(input->Get(i)); 
    std::string s(buffer,length);
    eci::protobuf::Action action;
    action.ParseFromString(s);
    std::cout << action.functiontype() << std::endl;
  }
}

这里有一个有趣的地方,在Node当中,是不允许worker线程访问v8的数据的。如果,想要获得v8当中的数据,只能进行一次copy,将副本传递到子线程当中。这样做有好有怀,不用考虑太多的同步问题,坏处则是,如果worker的input比较庞大,比如一张图片,那么这个拷贝的操作就会成为性能瓶颈,这个时候Buffer就会成为一个很好的媒介,如上所示node::Buffer::Data可以获得一个数组的指针,这个指针可以直接传递到worker线程,也就避免了数据拷贝来拷贝去的开销

<h3 id=“6.1”>性能评估之Google自吹自擂</h3>

![序列化数据size](http://chart.apis.google.com/chart?chtt=length&chf=c||lg||0||FFFFFF||1||76A4FB||0|bg||s||EFEFEF&chs=689x430&chd=t:207.0,211.0,211.0,226.0,231.0,231.0,264.0,264.0,300.0,353.0,359.0,370.0,378.0,399.0,419.0,448.0,465.0,470.0,475.0,475.0,526.0,919.0,2024.0&chds=0,2226.4&chxt=y&chxl=0:|scala|java|hessian|stax/woodstox|stax/aalto|json/google-gson|json/jackson-databind|protostuff-json|javolution xmlformat|xstream (stax with conv)|json (jackson)|JsonMarshaller|protostuff-numeric-json|thrift|binaryxml/FI|sbinary|java (externalizable)|protobuf|activemq protobuf|kryo|avro-specific|avro-generic|kryo-optimized&chm=N f,000000,0,-1,10&lklk&chdlp=t&chco=660000|660033|660066|660099|6600CC|6600FF|663300|663333|663366|663399|6633CC|6633FF|666600|666633|666666&cht=bhg&chbh=10&nonsense=aaa.png’ /> “序列化数据size”)

![序列化](http://chart.apis.google.com/chart?chtt=timeSerializeDifferentObjects&chf=c||lg||0||FFFFFF||1||76A4FB||0|bg||s||EFEFEF&chs=689x430&chd=t:2874.1185,2912.7375,3437.8375,5749.9665,6330.785,6777.6865,7223.819,7226.509,7302.9785,7584.8375,8011.9495,8018.866,8542.4285,8639.84,8704.781,10550.4115,13354.7855,15336.6385,16116.891,24618.395,25773.3065&chds=0,28350.637150000002&chxt=y&chxl=0:|java|JsonMarshaller|xstream (stax with conv)|binaryxml/FI|hessian|json/jackson-databind|sbinary|protostuff-json|stax/woodstox|avro-generic|protostuff-numeric-json|javolution xmlformat|thrift|protobuf|json (jackson)|activemq protobuf|stax/aalto|avro-specific|kryo|kryo-optimized|java (externalizable)&chm=N f,000000,0,-1,10&lklk&chdlp=t&chco=660000|660033|660066|660099|6600CC|6600FF|663300|663333|663366|663399|6633CC|6633FF|666600|666633|666666&cht=bhg&chbh=10&nonsense=aaa.png’ /> “序列化”)

![反序列化](http://chart.apis.google.com/chart?chtt=timeDeserializeAndCheckAllFields&chf=c||lg||0||FFFFFF||1||76A4FB||0|bg||s||EFEFEF&chs=689x430&chd=t:3043.0035,3998.4815,4308.2,4355.6565,4371.6045,4584.8715,4779.31,4924.946,6084.47,7185.3925,7366.9585,7948.7375,8082.8465,10567.319,10575.581,12161.0625,14528.3345,21082.57,26235.771,41115.133,71573.7965&chds=0,78731.17615&chxt=y&chxl=0:|java|JsonMarshaller|xstream (stax with conv)|hessian|binaryxml/FI|stax/woodstox|json/jackson-databind|javolution xmlformat|stax/aalto|thrift|protostuff-json|protostuff-numeric-json|json (jackson)|activemq protobuf|avro-generic|sbinary|protobuf|avro-specific|kryo|kryo-optimized|java (externalizable)&chm=N f,000000,0,-1,10&lklk&chdlp=t&chco=660000|660033|660066|660099|6600CC|6600FF|663300|663333|663366|663399|6633CC|6633FF|666600|666633|666666&cht=bhg&chbh=10&nonsense=aaa.png’ /> “反序列化”)

<h3 id=“6.2”>Pb VS Json</h3>

  • 优点
    • 传输效率高,Protocol Buffer序列化后的数据要远小于json
    • 可维护性高
      • Protocol Buffer可以直接反序列化为class。
      • Protocal Buffer有明确的声明文件,不同团队之间可以基于生命文件进行沟通。JSON格式及其灵活,一方修改,可能其他方面并不知道。在工程的扩展过程中野蛮生长可能性极大。
      • Protocal Buffer提供了向下兼容的方案
  • 缺点:
    • 数据本身的可读性比较差,调试起来不是很方便
  • 其他
    • 性能方面,不同的实现方案差异比较大,需要具体分析
    • 可用性方面,两种方案资料都比较丰富,现存的库也比较丰富
回到顶部