protobuf 编解码_protobuf编解码 - CSDN
  • 在很多很多时候被问起,为什么选择protobuf?最先被想起的回答的就是体积小、解析快。...本文针对实际的例子,来对protobuf编解码方式进行详细讲解。其中,.proto文件定义如下: syntax = "proto2"; ...

    在很多很多时候被问起,为什么选择protobuf?最先被想起的回答的就是体积小、解析快。那相比较于json、XML,为什么protobuf能够做到又小又快呢?

    归其原因,这与它的编解码方式有很大的关系。本文将走进protobuf的深层原理来进行剖析。

    本文实例源码github地址https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2019Q4/20191230


    实例

    本文针对实际的例子,来对protobuf的编解码方式进行详细讲解。其中,.proto文件定义如下:

    syntax = "proto2";
    
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }
    
    message PhoneNumber {
        required string number = 1;
        optional PhoneType type = 2;
    }
    
    message Address {
        optional string country = 1;
        optional string detail = 2;
    }
    
    message Person {
        required int32 id =1;
        required string name = 2;
        optional int32 age = 3;
        repeated string email = 4;
        repeated PhoneNumber phone = 5;
        optional Address address = 6;
    }
    

    保存的person信息为:

    id : 1
    name : 'zhangsan'
    age : 18
    email : ['1.qq.com', '2.qq.com']
    phone : [number: "123456", type: HOME, number: "234567", type: MOBILE]
    address : country: "China", detail: "Jiangsu"
    

    这段person信息,生成的一段二进制内容为:

    08 01 12 08 7A 68 61 6E 67 73 61 6E 18 12 22 08 31 2E 
    71 71 2E 63 6F 6D 22 08 32 2E 71 71 2E 63 6F 6D 2A 0A 
    0A 06 31 32 33 34 35 36 10 01 2A 0A 0A 06 32 33 34 35 
    36 37 10 00 32 10 0A 05 43 68 69 6E 61 12 07 4A 69 61 
    6E 67 73 75
    

    varint和ZigZag

    在protobuf中,主要使用的两种编码方式就是varintZigZag

    varint

    varint是一种可变长编码,使用1个或多个字节对整数进行编码,可编码任意大的整数,小整数占用的字节少,大整数占用的字节多,如果小整数更频繁出现,则通过varint可实现压缩存储。

    varint中每个字节的最高位bit称之为most significant bit(MSB),如果该bit为0意味着这个字节为表示当前整数的最后一个字节,如果为1则表示后面还有至少1个字节,可见,varint的终止位置其实是自解释的

    也就是说,每个字节的最高位表示后面还有没有字节,若为0表示后面没有字节,若为1表示后面有字节。而每个字节的低7位就是实际的值,并且使用小端的表示方法

    例如1,varint的表示方法就为:

    0000 0001
    

    ,而int本身是4字节的,采用varint的编码方式就省了三个字节。

    再例如300,varint表示为:

    10 0101100                          //300的二进制
    10101100 00000010                   //300的varint编码
    

    ,而int本身是4字节的,采用varint的编码方式就省了两个字节。

    ZigZag

    ZigZag编码:整数压缩编码 ZigZag

    优秀的压缩编码应该满足:高概率的码字字长应不长于低概率的码字字长。而一般情况下,使用较多的是小整数,那么较小的整数应使用更少的byte来编码。基于此思想,ZigZag被提出来。

    ZigZag按绝对值升序排列,将整数hash成递增的32位bit流,其hash函数为h(n)=(n<<1) ^ (n>>31);对应地long类型(64位)的hash函数为h(n)=(n<<1) ^ (n>>63)。整数的补码(十六进制)与hash函数的对应关系如下:

    n hex h(n) ZigZag(hex)
    0 00 00 00 00 00 00 00 00 00
    -1 ff ff ff ff 00 00 00 01 01
    1 00 00 00 01 00 00 00 02 02
    -2 ff ff ff fe 00 00 00 03 03
    2 00 00 00 02 00 00 00 04 04
    -64 ff ff ff c0 00 00 00 7f 7f
    64 00 00 00 40 00 00 00 80 80

    可以看出,Zigzag编码用无符号数来表示有符号数字,正数和负数交错,这就是zigzag这个词的含义了。

    其实,正数扩大成2倍,负数取反加1

    编码小结

    其实varint编码和ZigZag,都可以编码正数和负数,那为什么protobuf怎么抉择的呢?

    如果表示的都是正数,varint的方式编码会比ZigZag编码小很多;但如果表示的很多都是负数,由于负数的最高位为1,如果负数也使用varint编码就会出现一个问题,int32总是需要5个字节,int64总是需要10个字节。此时ZigZag的编码方式会更恰当。

    为了统一两种方式,并效仿varint的压缩优势,减少ZigZag的字节数。最终,sint32被编码为(n<<1) ^ (n>>31)对应的varint,sint64被编码为(n<<1) ^ (n>>63)对应的varint,这样,绝对值较小的整数只需要较少的字节就可以表示。

    因此,protobuf对于正数的编码采用varint,对于负数的编码采用ZigZag编码后的varint。

    其实,这句话这样说是不恰当的。因为,protobuf也无法自动识别正数负数并做出不同的编码方式的选择。采用的做法是,在.proto结构定义文件中,如果是int32、int64、uint32、uint64采用varint的方式,如果是sint32、sint64采用ZigZag编码后的varint的方式


    protobuf编解码

    对于序列化后字节流,需要回答的一个重要问题是从哪里到哪里是哪个数据成员。

    因此message的每个字段field在序列化时,一个field对应一个key-value对,整个二进制文件就是一连串紧密排列的key-value对,key也称为tag。采用这种key-value对的结构无需使用分隔符来分割不同的freld。对于可选的field,如果消息中不存在该field,那么在最终的message中就没有该field,这些特性都有助于节约消息本身的大小。

    key由wire type和FieldNumber两部分编码而成,具体地说,key=(field_number<<3)|wire_type,field_number部分指示了当前是哪个数据成员,通过它将cc和h文件中的数据成员与当前的key-value对应起来。也就是.proto文件中每个字段的标识号。

    key的最低3个bit为wire type,什么是wire type?如下表所示:

    Type Meaning Used For
    0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
    1 64-bit fixed64, sfixed64, double
    2 Length-delimi string, bytes, embedded messages, packed repeated fields
    3 Start group Groups (deprecated)
    4 End group Groups (deprecated)
    5 32-bit fixed32, sfixed32, float

    由于key的第三位最多表示8个值,而wire type目前的种类是6种。由于采用varint的编码方式,只剩下4位的空闲存放field_number,因此之前在定义每个字段的标识号的时候建议不要超过15。

    wire type被如此设计,主要是为了解决一个问题,如何知道接下来value部分的长度(字节数),如果:

    • wire type=0、1、5,编码为key+数据,只有一个数据,可能占数个字节,数据在编码时自带终止标记
    • wire type=2,编码为key+length+数据,length指示了数据长度,可能有多个数据,顺序排序

    需要注意的是:

    • 如果出现嵌套message,直接将嵌套message部分的编码接在length后即可
    • repeated后面接的字段,如果是个message,它重复出现多少次,编码时其key就会出现几次;如果接的是proto定义的字段,且以packed = true压缩存储时,只会出现1个key;如果不以压缩方式存储,其key也会出现多次。在proto3中,默认以压缩方式进行存储,proto2中则需要显式地声明。

    阅读二进制文件

    对于实例中的二进制文件,逐个解析:

    08      // (1<<3)|0,1为id的field_bumber,0为id对应的wire type
    01      // 0x01,id为1
    
    12      // (2<<3)|1,2为name的field_bumber,1为name对应的wire type
    08      // name字段的字符串长度
    7A68616E6773616E      // "zhangsan"的ASCII码
    
    18      // (3<<3)|0,3为age的field_bumber,0为age对应的wire type
    12      // 0x12,age为18
    
    22      // (4<<3)|2,4为email的field_bumber,2为email对应的wire type
    08      // email字段的字符串长度
    312E71712E636F6D      // "1.qq.com"的ASCII码
    22      // (4<<3)|2,4为email的field_bumber,2为email对应的wire type
    08      // email字段的字符串长度
    322E71712E636F6D      // "2.qq.com"的ASCII码
    
    2A      // (5<<3)|2,5为phone的field_bumber,2为phone对应的wire type
    0A      // 0x10,phone的长度为10,1+1+6+1+1
    0A      // (1<<3)|2,1为number的field_bumber,2为number对应的wire type
    06      // number字段的字符串长度
    313233343536      // "123456"的ASCII码
    10      // (2<<3)|1,2为type的field_bumber,1为type对应的wire type
    01      // enum为1,表示HOME
    
    2A      // (5<<3)|2,5为phone的field_bumber,2为phone对应的wire type
    0A      // 0x10,phone的长度为10,1+1+6+1+1
    0A      // (1<<3)|2,1为number的field_bumber,2为number对应的wire type
    06      // number字段的字符串长度
    323334353637      // "234567"的ASCII码
    10      // (2<<3)|1,2为type的field_bumber,1为type对应的wire type
    00      // enum为0,表示MOBILE
    
    32      // (6<<3)|2,6为address的field_bumber,2为address对应的wire type
    10      // 0x10,address的长度为16,1+1+5+1+1+7
    0A      // (1<<3)|2,1为country的field_bumber,2为country对应的wire type
    05      // country字段的字符串长度
    4368696E61      // "China"的ASCII码
    12      // (2<<3)|2,2为detail的field_bumber,2为detail对应的wire type
    07      // detail字段的字符串长度
    4A69616E677375      // "Jiangsu"的ASCII码
    

    protobuf的又小又快

    数据变小一点

    上文讲解的数据的varint编码方式,肯定能减少数据的大小,这点不再赘述。

    诸如json、XML等数据,中间存在大量的冗余字符,比如{、}、"、<、>等等,为了减少数据量,我们可以暴力一点,直接把这些冗余信息去掉。但是会带来一些问题,就是当这段数据发送给接收端,接收端怎么知道每个value对应哪个key呢?

    比较好的解决方案是:事先跟接收端约定好有哪些字段,顺序是什么,然后接收端按照这个规则对应起来。这就是.proto文件的内容。

    但是,随之而来又有一个问题:.proto文件中的optional字段,如果没有没有该字段的信息,其实是不必要传递这个字段的。但此时在接收端,解析数据并按照顺序进行字段匹配的时候就会出问题。

    显然已经乱套了,为了保证能够正确的配对,我们可以使用tag技术。也就是说,每个字段我们都用tag-value的方式来存储的,在tag当中记录两种信息,一个是value对应的字段的编号,另一个是value的数据类型(比如是整形还是字符串等),因为tag中有字段编号信息,所以能够正确的配对。

    可能你会问,使用tag的话,会增加额外的空间,这跟json的key-value有什么区别吗?

    这个问题问的好,json中的key是字符串,每个字符就会占据一个字节,所以像name这个key就会占据4个字节,但在protobuf中,tag使用二进制进行存储,一般只会占据一个字节。相比较而言,tag所消耗的额外内存空间相对而言小很多。

    即归纳成三点:

    • varint和ZigZag的编码方式
    • 隔断冗余信息的剔除
    • tag-value方式的存储,tag采用二进制进行存储

    解析变快一点

    protobuf,它只需要简单地将一个二进制序列,按照指定的格式读取到C++对应的结构类型中就可以了。消息的decoding过程也可以通过几个位移操作组成的表达式计算即可完成。而对于字符串、自定义对象类型的数据,protobuf在存储的时候,也存储了该数据的字节长度,读取起来也非常快。


    相关阅读

    展开全文
  • Protobuf3 编解码

    2019-01-05 21:52:11
    我们已经基本能够使用Protocol Buffers生成代码,编码,解析,输出及读入序列化数据。该篇主要讲述PB message的底层二...Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Enco...

    我们已经基本能够使用Protocol Buffers生成代码,编码,解析,输出及读入序列化数据。该篇主要讲述PB message的底层二进制格式。不了解该部分内容,并不影响我们在项目中使用Protocol Buffers,但是了解一下PB格式是如何做到smaller这一层,确实是很有必要的。Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法。

    1.什么是 Varint
    (1).Varint 是一种紧凑的表示数字的方法,它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。
    比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。
    (2).Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。
    因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010


    下图演示了 Protocol Buffer 如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian 的方式。
    Varint 编码

    图 6. Varint 编码

    消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示:
    Message Buffer

    图 7. Message Buffer

    采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。
    二进制格式的message使用数字标签作为key,Key 用来标识具体的 field,在解包的时候,Protocol Buffer 根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 field。
    将 message编码后,key-values被编码成字节流存储。在message解码时,PB 解析器会跳过(忽略)不能够识别的字段,所以,message即使增加新的字段,也不会影响老程序代码,因为老程序代码根本就不能识别这些新添加的字段。
    上边我们说,“二进制格式的message使用数字标签作为key”,此处的数字标签,并非单纯的数字标签,而是数字标签与传输类型的组合,根据传输类型能够确定出值的长度。

    key的定义:

    (field_number << 3) | wire_type

    可以看到 Key 由两部分组成,第一部分是 field_number,第二部分为 wire_type。表示 Value 的传输类型,也就是说:key中的后三位,是值的wire_type类型。
    Wire Type 类型如下表所示:

    TypeMeaningUsed For
    0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
    1 64-bit fixed64, sfixed64, double
    2 Length-delimi string, bytes, embedded messages, packed repeated fields
    3 Start group Groups (deprecated)
    4 End group Groups (deprecated)
    5 32-bit fixed32, sfixed32, float

    举个例子来分析protobuf数据编码和解码,如下所示:

    message Test1 {
        required int32 a = 1;    
    }
    //.......protobuf读写操作..........
    Test1 test;
    test.set_a(150);
    //.....将数据序列化到文件.....

    写入message后,用UltraEdit打开,二进制格式查看,我们看到最终输出文件中包含三个数字:08 96 01(十六进制),这是如何得来的呢?
    1.首先来解析tag

    2.至此我们知道数字的field_number=1,值类型为varint。根据上面讲解来解码96 01,即为150:

    96 01 = 1001 0110  0000 0001
           → 000 0001  ++  001 0110 (drop the msb and reverse the groups of 7 bits)
           → 10010110
           → 2 + 4 + 16 + 128 = 150

    注意:数值部分,低位在前,高位在后。

     

    2.protobuf负数表示方式
    在计算机内,一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用 Varint 表示一个负数,那么一定需要 10 个 byte长度。为此 Google Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码。将所有整数映射成无符号整数,然后再采用varint编码方式编码,这样绝对值小的整数,编码后也会有一个较小的varint编码值。
    Zigzag 编码用无符号数来表示有符号数字,正数和负数交错,这就是 zigzag 这个词的含义了。

    图 8. ZigZag 编码

    使用 zigzag 编码,绝对值小的数字,无论正负都可以采用较少的 byte 来表示,充分利用了 Varint 这种技术。
    其他的数据类型,比如字符串等则采用类似数据库中的 varchar 的表示方法,即用一个 varint 表示长度,然后将其余部分紧跟在这个长度部分之后即可。

    Zigzag映射函数为:

    Zigzag(n) = (n << 1) ^ (n >> 31);    //n为sint32时
    Zigzag(n) = (n << 1) ^ (n >> 63);    //n为sint64时

    按照这种方法,-1将会被编码成1,1将会被编码成2,-2会被编码成3,如下表所示:

    Signed OriginalEncoded As
    0 0
    -1 1
    1 2
    -2 3
    2 4
    -3 5
    2147483647 4294967294
    -2147483648 4294967295

     

    3.Non-varint 数字
    Non-varint数字比较简单,double 、fixed64 的Wire Type:1,在解析式告诉解析器,该类型的数据需要一个64位大小的数据块即可。同理,float和fixed32的Wire Type:5,给其32位数据块即可。两种情况下,都是高位在后,低位在前。

    4.String类型
    Wire Type:2的数据,是一种指定长度的编码方式:key+length+content,key的编码方式是统一的,length采用varints编码方式,content就是由length指定长度的Bytes。定义如下的message格式:

    message Test2 {
        required string b = 2;
    }

    设置该值为"testing",二进制格式查看:12 07 74 65 73 74 69 6e 67
    红色字节为“testing”的UTF8代码,此处,key是16进制表示的,所以展开是:12 -> 0001 0010,后三位010为wire type = 2,0001 0010右移三位为0000 0010,即tag=2。
    length此处为7,后边跟着7个bytes,即我们的字符创"testing"。

    字段顺序
    简单来说只有两点:
        编码/解码与字段顺序无关,这一点由key-value机制就能保证
        对于未知的字段,编码的时候会把它写在序列化完的已知字段后面

    展开全文
  • 图解Protobuf编码

    2016-11-21 14:04:52
    图解Protobuf编码Protobuf是Google发布的消息序列化工具。Protobuf定义了消息描述语法(proto语法)和消息编码格式,并且提供了主流语言的代码生成器(protoc)。本文仅讨论Protobuf消息编码格式,并且假定读者已经...

    图解Protobuf编码

    Protobuf是Google发布的消息序列化工具。Protobuf定义了消息描述语法(proto语法)和消息编码格式,并且提供了主流语言的代码生成器(protoc)。本文仅讨论Protobuf消息编码格式,并且假定读者已经熟悉Protobuf消息描述语法(proto2或者proto3)。


    基本编码规则

    Protobuf消息由字段(field)构成,每个字段有其规则(rule)、数据类型(type)、字段名(name)、tag,以及选项(option)。比如下面这段代码描述了由10个字段构成的Test消息:

    test.proto

    序列化时,消息字段会按照tag顺序,以key+val的格式,编码成二进制数据。以下面这段Java代码为例:

    byte[] data = Test.newBuilder()
      .setA(3).setB(2).setC(1)
      .build().toByteArray();

    序列化之后,可以把data里的数据想象成下面这样:

    这里写图片描述

    proto2语法定义了3种字段规则:required、optional、repeated。proto3语法去掉了required规则,只剩下optional(默认)和repeated两种。由上图可知,如果没有给optional和repeated字段赋值,那么字段是不会出现在序列化后的数据中的。详细的编码规则,请继续阅读。

    数据划分

    Protobuf消息序列化之后,会产生二进制数据。这些数据(精确到bit)按照含义不同,可以划分为6个部分:MSB flag、tag、编码后数据类型(wire type)、长度(length)、字段值(value)、以及填充(padding)。后文会图解这些部分的具体含义,这里先约定好图中消息各部分使用的颜色:

    colors

    Key+Value

    前面说过,消息的每一个字段,都会以key+val的形式,序列化为二进制数据。val比较好猜测,那么key具体是什么呢?答案是这样:key = tag << 3 | wire_type。也就是说,key的前3个比特是wire type,剩下的比特是tag值。Protobuf支持丰富的数据类型,但是编码之后,只剩下Varint(0)、64-bit(1)、Length-delimited(2)和32-bit(5)这4种(还有两种已经废弃了,本文不讨论)类型,用3个比特来表示,足够了。以前面定义的Test消息为例:

    byte[] data = Test.newBuilder()
      .setA(3).setB(2).setC(1)
      .build().toByteArray();

    序列化之后的数据有6个字节,是下面这个样子:

    abc

    Varint

    用3个bit来表示wire type是够了,但是tag是用剩下的5个bit来表示吗?tag难道不能超过32(2^5)吗?由上图已经知道,答案是否!为了用尽可能少的字节编码消息,Protobuf在多处都使用了Varint这种格式。比如数据类型里的int32、int64,以及tag值和后面将要解释的length值,都使用Varint类型存储。那么Varint到底有什么神奇之处呢?也没有,其实就是用每个字节的前7个bit来表示数据,而最高位的bit(MSB,Most Significant Bit)则用作记号(flag)。文字不太好描述,看一个例子:

    byte[] data2 = Test.newBuilder()
      .setJ(1) // tag=16
      .build().toByteArray();

    由于tag是按Varint编码的,所以要扣掉一个bit(MSB)。再减去wire type占用的3个比特,那么第一个字节里,留给tag值的,实际只剩下4个比特,只能表示0到15。由于Test消息j字段的tag值是16,所以需要两个字节才能表示j字段的key。data2如下图所示(重要的bit进行了旋转,以示提醒):

    tag16

    64-bit和32-bit

    前面说了,为了节省字节数,tag、length,以及int32、int64等数据类型都是用Varint编码的。那么这种编码方式有什么坏处吗?主要有2处。第一,不利于表示大数。对于比较小的数来说,以0到127为例,用Varint很划算。以浪费1bit和少量额外的计算为代价,只要1个字节就可以表示。但是对于比较大的数,就不划算了。以int32为例,大于2^(4*7) - 1的数,需要用5个字节来表示。看一个例子:

    byte[] data3 = Test.newBuilder()
      .setA(268435456) // 2^28
      .build()
      .toByteArray();

    序列化之后的数据如下图所示:

    268435456

    也就是说,如果某个消息的某个int字段大部分时候都会取比较大的数,那么这个字段使用Varint这种变长类型来编码就没什么好处。对于这种情况,Protobuf定义了64-bit和32-bit两种定长编码类型。使用64-bit编码的数据类型包括fixed64、sfixed64和double;使用32-bit编码的数据类型包括fixed32、sfixed32和float。以Test消息e字段(fixed32)为例:

    byte[] data4 = Test.newBuilder()
      .setE(268435456) // 2^28
      .build()
      .toByteArray();

    序列化之后的数据如下图所示:

    fixed32

    ZigZag

    Varint编码格式的第二缺点是不适合表示负数,以int32和-1为例:

    byte[] data5 = Test.newBuilder()
      .setA(-1)
      .build()
      .toByteArray();

    Protobuf想让int32和int64在编码格式上兼容,所以-1需要占用10个字节,如下图所示:

    n1

    为了克服这个缺陷,Protobuf提供了sint32和sint64两种数据类型。如果某个消息的某个字段出现负数值的可能性比较大,那么应该使用sint32或sint64。这两种数据类型在编码时,会先使用ZigZig编码将负数映射成正数,然后再使用Varint编码。ZigZag编码规则如下图所示:

    zigzag

    以Test消息的d字段(sint32)为例:

    byte[] data6 = Test.newBuilder()
      .setD(-2) // sint32
      .build()
      .toByteArray();

    序列化之后的数据如下图所示:

    这里写图片描述

    Length-delimited

    如前所述,64-bit和32-bit是定长编码格式,长度固定。Varint是变长编码格式,长度由字节的MSB决定。Length-delimited编码格式则会将数据的length也编码进最终数据,使用Length-delimited编码格式的数据类型包括string、bytes和自定义消息。以string为例:

    byte[] data7 = Test.newBuilder()
      .setF("hello") // string
      .build()
      .toByteArray();

    序列化之后的数据如下图所示:

    hello

    下面是自定义消息的例子:

    byte[] data8 = Test.newBuilder()
      .setI(Test.newBuilder().setA(1))
      .build()
      .toByteArray();

    序列化之后的数据如下图所示:

    i

    repeated

    前面讨论的字段都是optional类型,最多只有一个val,但是repeated字段却可以有多个val。那么repeated字段是如何序列化的呢?以Test消息的g字段为例:

    byte[] data9 = Test.newBuilder()
      .addG(1).addG(2).addG(3)
      .build()
      .toByteArray();

    序列化之后的数据如下图所示:

    repeated

    可见,repeated字段就是简单的把每个字段值依次序列化而已。

    packed

    如果repeated字段包含的val比较多,那么每个val都带上key是不是比较浪费呢?是的,所以Protobuf提供了packed选项,以Test消息的h字段为例:

    byte[] data10 = Test.newBuilder()
      .addH(1).addH(2).addH(3) // packed
      .build()
      .toByteArray();

    序列化之后的数据如下图所示:

    packed

    可见,如果repeated字段设置了packed选项,则会使用Length-delimited格式来编码字段值。


    结束。

    展开全文
  • Netty自定义protobuf编解码器 Netty使用protobuf作传输,Netty内部已经实现默认的Netty编解码器,在Java服务和客户端之间调用是没问题,如果项目有异构语言的客户端连接Netty服务,使用protobuf作传输,那么Netty...

    Netty自定义protobuf编解码器

    Netty使用protobuf作传输,Netty内部已经实现默认的Netty编解码器,在Java服务和客户端之间调用是没问题,如果项目有异构语言的客户端连接Netty服务,使用protobuf作传输,那么Netty的默认编解码器就不适用了。这里自定义了编解码器。这里还有一点问题,自定义的协议头可能存在不同语言大小端的问题,想到的解决办法就是将协议头也作成一个protobuf就能解决了。

    协议头
    为防止粘包,在protobuff上层添加一层交互协议,每个protobuff包前面加六个字节的协议头定义。其中0-3 表示一个int类型protobuff字节大小,4为保留字段,5为数据类型。

    0 1 2 3 4 5
    0-8位 9-16位 17-24位 25-32位 保留字段 数据类型

    package com.iscas.optocon.distribute.nettyserver.handler;
    
    import com.google.protobuf.MessageLite;
    import com.iscas.optocon.protobuf.Message;
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.ByteToMessageDecoder;
    import io.netty.util.ReferenceCountUtil;
    
    import java.util.List;
    
    /**
     * //TODO
     *
     * @author zhuquanwen
     * @vesion 1.0
     * @date 2018/7/21 21:31
     * @since jdk1.8
     */
    public class CustomProtobufDecoder extends ByteToMessageDecoder {
    
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
            while (in.readableBytes() > 6) { // 如果可读长度小于包头长度,退出。
                in.markReaderIndex();
    
                // 获取包头中的body长度
                byte l0 = in.readByte();
                byte l1 = in.readByte();
                byte l2 = in.readByte();
                byte l3 = in.readByte();
                int s0 = (int) (l0 & 0xff);
                int s1 = (int) (l1 & 0xff);
                int s2 = (int) (l2 & 0xff);
                int s3 = (int) (l3 & 0xff);
                s1 <<= 8;
                s2 <<= 16;
                s3 <<= 24;
                int length = (int) (s0 | s1 | s2 | s3);
    
                // 获取包头中的protobuf类型
                in.readByte();
                byte dataType = in.readByte();
    
                // 如果可读长度小于body长度,恢复读指针,退出。
                if (in.readableBytes() < length) {
                    in.resetReaderIndex();
                    return;
                }
    
                // 读取body
                ByteBuf bodyByteBuf = in.readBytes(length);
    
                byte[] array;
                int offset;
    
                int readableLen= bodyByteBuf.readableBytes();
                if (bodyByteBuf.hasArray()) {
                    array = bodyByteBuf.array();
                    offset = bodyByteBuf.arrayOffset() + bodyByteBuf.readerIndex();
                } else {
                    array = new byte[readableLen];
                    bodyByteBuf.getBytes(bodyByteBuf.readerIndex(), array, 0, readableLen);
                    offset = 0;
                }
    
                //反序列化
                MessageLite result = decodeBody(dataType, array, offset, readableLen);
    //            ctx.fireChannelRead((Message.MessageBase)result);
                out.add(result);
                ReferenceCountUtil.release(bodyByteBuf);
            }
        }
    
        public MessageLite decodeBody(byte dataType, byte[] array, int offset, int length) throws Exception {
            if (dataType == 0x00) {
                return Message.MessageBase.getDefaultInstance().
                        getParserForType().parseFrom(array, offset, length);
    
            }
    //        else if (dataType == 0x01) {
    //            return OptionTickOuterClass.OptionTick.getDefaultInstance().
    //                    getParserForType().parseFrom(array, offset, length);
    //        }
    
            return null; // or throw exception
        }
    }
    
    
    package com.iscas.optocon.distribute.nettyserver.handler;
    
    import com.google.protobuf.MessageLite;
    import com.iscas.optocon.distribute.util.DataUtils;
    import com.iscas.optocon.protobuf.Message;
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandler;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.handler.codec.MessageToByteEncoder;
    
    /**
     * //TODO
     *
     * @author zhuquanwen
     * @vesion 1.0
     * @date 2018/7/21 21:25
     * @since jdk1.8
     */
    @ChannelHandler.Sharable
    public class CustomProtobufEncoder extends MessageToByteEncoder<MessageLite> {
    
    //    HangqingEncoder hangqingEncoder;
    //
    //    public CustomProtobufEncoder(HangqingEncoder hangqingEncoder)
    //    {
    //        this.hangqingEncoder = hangqingEncoder;
    //    }
    
        @Override
        protected void encode(
                ChannelHandlerContext ctx, MessageLite msg, ByteBuf out) throws Exception {
    
    
            byte[] body = msg.toByteArray();
    //        byte[] header = encodeHeader(msg, body.length);
            byte[] header = DataUtils.encodeHeader(msg, body.length);
            out.writeBytes(header);
            out.writeBytes(body);
    
            return;
        }
    
    
    }
    
    

    其他语言想与Netty服务通信,可以使用上面定义的协议头解析。

    上一篇 Netty对象传输
    下一篇 使用jprotobuf实现Netty编解码器

    展开全文
  • Google Protobuf 编解码

    2020-05-30 21:31:06
    Protobuf 全称:Google Protocol Buffers,由谷歌开源而来,经谷歌内部测试使用。它将数据结构以 .proto 文件进行描述,通过代码生成工具...【2】高效的编解码性能,编码后的消息更小,有利于存储和传输; 【3】语...
  • Netty使用Protobuf编解码

    2018-09-12 09:54:04
    Protobuf是一个灵活、高效、结构化的数据序列化框架,相比于XML等传统的序列化工具,它更小、更快、更简单。Protobuf支持数据结构化一...下面结合一个例子看看在Netty如何使用Protobuf对POJO对象进行编解码。 1.Pr...
  • Netty结合Protobuf编解码

    2019-01-29 16:36:04
    一般在使用netty时,数据传输的时候都会选择对传输的数据进行编解码,编码后的数据变小, 有利于在有限的带宽下传输更多的数据。 由于java本身序列化的缺点较多(无法跨语言,序列化后的码流太大,序列化的性能太低...
  • NULL 博文链接:https://shihuan830619.iteye.com/blog/2265589
  • Java的序列化 Java提供的序列化机制,涉及到两个对象输入输出流类,ObjectInputStream,ObjectOutputStream...反序列化,将从远程服务读取到的ByteBuffer对象或者字节数组解码为Java对象。 Java序列化的缺点: 1...
  • Google 的protobuf 在业界非常流行,很多的商业项目选址使用protobuf作为编解码框架,这里一起回顾一下protobuf 框架的优点: 1、结构化数据存储(类似XML和JSON) 2、高效编码性能 3、语言无法、平台无法、...
  • 前面介绍了通过Java原生的序列化来实现编解码在网络传输,也说到了其诸多缺点,下面介绍另外一种业界现在非常流行的编解码方式:Protobuf。 、
  • Protobuf3 + Netty4: 在socket上传输多种类型的protobuf数据 Protobuf序列化的字节流数据是不能自描述的,当我们通过socket把数据发送到Client时,Client必须知道发送的是什么类型的数据,才能正确的反序列...
  • 一、配置protobuf环境支持 idea安装protobuf插件 preference -> plugins ->搜索protobuf support安装 环境安装protoc编译工具 1.下载protoc: https://github.com/protocolbuffers/protobuf/releases 2.选择...
  • 在掌握了以上内容后,下面我们使用Netty的Protobuf编解码框架来进行客户端和服务端的开发,通过一个图书订阅的小案例就行讲解说明,那我们就开始吧! 废话不说直接上代码 整体项目工程 代码全部都完整给出,强烈建议...
  • Google Protobuf编解码机制
  • 8.1.2 Protobuf编解码开发Protobuf的类库使用比较简单,下面我们就通过对SubscrjbeReqProto进行编解码来介绍Protobuf的使用。 8-1 Protobuf入门TestsubscrjbeReqProto 1 package lqy7_protobuf_140; 2 3 ...
  • 下面我们通过简单的例子来学习如何使用protobuf对POJO对象进行编解码、学习如何与Netty结合使用,及学习两个进程间进行通信和数据交换。 1.1、下载安装protoc 编译工具 在进行开发 protoc 之前,你需要首先在你...
  • 《从零开始搭建游戏服务器》自定义兼容多种Protobuf协议的编解码器 直接在protobuf序列化数据的前面,加上一个自定义的协议头,协议头里包含序列数据的长度和对应的数据类型,在数据解包的时候根据包头来进行反序列...
  • Google protobuf 编码解码

    2019-10-19 14:02:30
    官网 ... Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data. ...protobuf是一个跨语言,平台的,可以序列化结构性数据的可扩展解决...
1 2 3 4 5 ... 20
收藏数 4,638
精华内容 1,855
关键字:

protobuf 编解码