码迷,mamicode.com
首页 > 其他好文 > 详细

Google protocol buffer 使用和原理浅析 - 附带进阶使用方式

时间:2016-07-03 19:11:17      阅读:309      评论:0      收藏:0      [点我收藏+]

标签:

Protocol Buffer

??Google Protocol Buffer又简称Protobuf,它是一种很高效的结构化数据存储格式,一般用于结构化数据的串行化,简单说就是我们常说的数据序列化。这种序列化的协议非常轻便高效,而且是跨平台的,目前已支持多种主流语言(3.0版本支持C++, JAVA, C#, OC, GO, PYTHON等)。
??通过这种方式序列化得到的二进制流数据比传统的XML, JSON等方式的结果都占用更小的空间,并且其解析效率也更高,用于通讯协议或数据存储领域是非常好的。
??再者,其使用的方式也非常简单,我们只需要预先定义好消息(message)的数据格式,然后通过其提供的compiler即可生成对应的文件,在那些文件里定义和实现了操作这个数据结构相关域的setter/getter方法,我们只需要使用这些方法设置该数据结构的域,然后通过序列化方法即可得到需要的结果(二进制数据流)。

一、优缺点

优点挺多的,以下简单例举几个好鸟。

  1. 更小,更快,更简单。更小是因为它的数据存储的很紧凑,与使用XML定义的数据相比较,其空间小3-4倍。后面从它的实现原理上也可以了解到为什么它占用空间会更小。
  2. “向后”兼容性好。不必破坏已部署的、依靠老数据格式的程序就可以对数据结构进行升级。所以不必担心因为消息结构的改变而造成大规模的代码重构或迁移问题。
  3. 语义清晰,不需要自己实现类似XML解析器那样的东西。只要使用Protobuf的compiler就可以生成对应的用于序列化和反序列化的对象。

??其最大的缺点应该就是它缺乏自描述性,所以它不适合用来描述数据结构。与XML不同的就是,我们可以从XML的文件中直接很清晰的看出数据的层次结构等,而Procolbuf的结果都是二进制流不可读的,我们只能通过.proto文件来了解其数据结构。

二、Protocol Buffer的使用

??Protobuf支持多语言,这里我们作为例子讲解的话主要解释的是C++语言上的使用方式,= =。其实不管啥语言好像都类似差不多,八九不离十的了。= =。这里我就简单的做一下介绍就好了,其实详细的介绍和使用都可以在官网上的指南查到【Language Guide】。

2.1 第一步

??这里例子使用的语言是C++,Protobuf的版本是2.6.1,windows平台上跑的。这里直接甩出官网的链接,可以去上面下载【protocol-buffers】,因为这个也是个开源项目(用C++写的),在github上也可以找到,而且上面也有3.0beta版本之类的可以去看看。

??安装完了我们首先来看一波.proto定义文件的内容。这个消息(message)中定义了两个int和一个string类型的域。一般实际上的消息要比这个要复杂的多,不过好在protobuf也支持比较复杂的消息结构。

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

2.2 指定字段的类型

下面的表格列出了消息里域允许的字段类型。

技术分享

2.3 指定字段的规则

对于message中的字段可以指定三种规则,而且也都很简单:

  1. required:表示该字段的值是必须要存在的,且只有一个。
  2. optional:表示该字段的值是可选的(不存在或有且只有一个)。
  3. repeated:表示该字段值可以有零或多个,且是有序的(即添加的顺序)。

2.4 其他部分

??在一个.proto文件里可以定义多个message,且message可以嵌套(这点极大增强了灵活性啊)。message中对于字段的注释和我们平时代码的注释类似,使用 // 的方式就好了。

2.5 使用

??当定义好了.proto文件,并且下载安装好对应版本的compile后,执行以下命令可以生成对应的.h和.cc文件。
其中$SRC_DIR表示希望生成的文件所在的目录,以及对应.proto文件所在目录的位置。

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

??然后将生成的头文件引入要工程项目中,直接调用里面对应的方法就好了。= =。这些最基本的就不多说了,下面主要是扯一下它的实现原理和一个进阶使用。

??如果对于安装和使用还有神马疑问,可以参考一下这些文章。(个人还是优先推荐官方文档)

  1. https://developers.google.com/protocol-buffers/docs/cpptutorial#defining-your-protocol-format
  2. http://colobu.com/2015/01/07/Protobuf-language-guide/ 【官方文档翻译版】???
  3. http://www.jianshu.com/p/b1f18240f0c7

三、原理简介

??首先通过上面的简单使用应该可以了解到,实际上protobuf就是帮我们生成对应的消息类,且每个类中包含了对指定字段的setter, getter方法,以及序列化和反序列化整个消息类的serializeparse方法,对于使用者来说只需要简单调用这些方法就可以实现消息的序列化和反序列化操作了。
??为了更深入了解其序列化和反序列化的原理的话,就要先了解其组织数据的方式。

3.1 TLV

??实际上protobuf使用一种类似((T)([L]V))的形式来组织数据的,即Tag-Length-Value(其中Length是可选的)。每一个字段都是使用TLV的方式进行序列化的,一个消息就可以看成是多个字段的TLV序列拼接成的一个二进制字节流。其实这种方式很像Key-Value的方式,所以Tag一般也可以看做是Key。由上可知,这种方式组织的数据并不需要额外的分隔符来划分数据,所以其可以减低序列化结果的大小。

??下面这图有点不太准确,不过可以凑合着理解一下。

技术分享

??Value的值很自然知道就是字段的值,那么Tag值是什么呢?在.proto文件中定义的每一个字段都需要声明其数据类型,其还表明该字段是可变长度还是固定长度,这部分一般称为wire_type。此外, 每个字段都有一个filed值,这个值代表该字段是message里的第几个值,一般称为field_num。

required string query = 1 
//比如说这里字段query为可变长的string类型,其field = 1,是消息中的第一个值;

在Protobuf中,数据类型是进行了划分的,其中wire_type主要是以下几种类型。

  1. Varint是一种比较特殊的编码方式,后面会再介绍。
  2. FixedXXX是固定长度的数字类型。
  3. Length-delimited是可变长数据类型,常见的就是string, bytes之类的。
enum WireType {
    WIRETYPE_VARINT           = 0,
    WIRETYPE_FIXED64          = 1,
    WIRETYPE_LENGTH_DELIMITED = 2,
    WIRETYPE_START_GROUP      = 3,
    WIRETYPE_END_GROUP        = 4,
    WIRETYPE_FIXED32          = 5,
};

技术分享

??了解了wire_type的含义后,就可以知道Tag是怎么解析的。= =。就是结合移位操作和或操作就可以判断出其是哪种数据类型了。= =。这里可能有人会疑惑field_num左移3位后期会不会导致数据丢失,实际上我们可以假设field_num是uint32类型的,其左移3位后,可以表示的数范围为(0 ~ 2^29-1)这么大的范围足够表示message里字段数了吧!(从枚举的变量中可以知道wire_type只需要3位就可以表示了)。

key = field_num << 3 || wire_type;

3.2 Varint

??Varint是一种紧凑的表示数字的方式。它可以用一个或多个字节来表示一个数字,其中值越小的数字需要的字节数越少。Varint中每一个字节的最高位bit都是有特殊含义的,如果其值为1,则表示下一个字节也是该数字的一部分,如果其值为0,则表明该数字到这一个字节就结束了。
??通常情况下一个int32类型的数字,一般需要4个字节来表示。使用Varint方式编码的话,对于比较小的数字,比如说-128~127之间的数字则只需要一个字节,而如果是300(下图有解释),则需要两个字节来表示。然而其也有不好的地方,比如说对于一个大数字,其最多可能需要5个字节来表示,但从概率统计的角度来说,绝大多数情况下采用Varint编码可以减少字节数来表示数字。

技术分享

??在计算机里,一个负数会被表示为一个很大的整数(- -,就是最高位一般为符号位,负数最高位为1)。如果采用Varint来编码的话则一定会需要5个字节了。所以Google protocol buffer 定义了sint32, sint64这些数据类型,其采用zigzag编码(见下图)。这样无论是正数还是负数,只要其绝对值比较小的时候需要的字节数就少,可以充分发挥Varint编码的优势。

技术分享

3.3 序列化

??protobuf生成的类中,其继承体系涉及的主要是::google::protobuf::MessageLiteMessage这两个类,其中Message::google::protobuf::MessageLite的子类。我们自动生成的类可能继承自这两个类中的一个,这取决于在proto描述文件中的配置,如果设置option optimize_for = LITE_RUNTIM,则编译生成的类继承自::google::protobuf::MessageLite。这两个类都拥有基本的功能的代码,而Message是扩展出来的子类,增加了一些特性功能,然而实际中如果用不到这些功能,则开启这个优化可以使得我们生成的文件更小。

现在来了解一下序列化的过程。先看一段代码:

//序列化接口,传入一个输出流参数
void ReqBody::SerializeWithCachedSizes(::google::protobuf::io::CodedOutputStream* output) const {
  //这个message中有一个可选参数叫msg_set_req,其field_num = 1;
  //optional message msg_set_req = 1;
  //先判断该字段是否设置,如果设置则调用相应函数
  if (has_msg_set_req()) {
        ::google::protobuf::internal::WireFormatLite::WriteMessage(1, this->msg_set_req(),output);
    }
}

//判断该值是否已经设置 
inline bool ReqBody::has_msg_set_req() const {
    return (_has_bits_[0] & 0x00000001u) != 0;
}

??在代码中看到序列化就是判断某些字段是否已经设置了值,如果设置了值就调用相应的函数写出该字段。如果找一个包括多个字段的看的话,其SerializeWithCachedSizes方法中应该会包含多个类似上面的if()操作。然后还有很多类似判断该字段是否已经设置的内联函数。
??大家应该还注意到了has_bits这个数组,这个数组标示了其中某个字段是否已经设置了值,这个数组在反序列化是也会被创建和设置,然后就像上面那函数一样用于判断某个字段是都已经设置了值。
??通过查看protobuf源代码的wire_format_lite.h头文件中的定义,会发现针对不同类型的数据类型,都有对应的writeXXX方法。

// Write fields, including tags.
static void WriteInt32   (field_number,  int32 value, output);
static void WriteInt64   (field_number,  int64 value, output); 
static void WriteUInt32  (field_number, uint32 value, output);
static void WriteUInt64  (field_number, uint64 value, output);
static void WriteSInt32  (field_number,  int32 value, output);
static void WriteSInt64  (field_number,  int64 value, output);
static void WriteFixed32 (field_number, uint32 value, output);
static void WriteFixed64 (field_number, uint64 value, output);
static void WriteSFixed32(field_number,  int32 value, output);
static void WriteSFixed64(field_number,  int64 value, output);
static void WriteFloat   (field_number,  float value, output);
static void WriteDouble  (field_number, double value, output);
static void WriteBool    (field_number,   bool value, output);
static void WriteEnum    (field_number,    int value, output);

static void WriteString(field_number, const string& value, output);
static void WriteBytes (field_number, const string& value, output);

static void WriteGroup(field_number, const MessageLite& value, output);
static void WriteMessage(field_number, const MessageLite& value, output);

??然后我们来看一下其中数值类型,字符类型和message类型的字段的具体writeXXX方法,就能更清楚的了解TLV这种序列化方式了。= =。代码很直白简单~~~

//数值类型的字段,这里是int32
void WireFormatLite::WriteInt32(int field_number, int32 value, io::CodedOutputStream* output) {
  //tag
  WriteTag(field_number, WIRETYPE_VARINT, output);
  //这里应该是int32这些固定长度的数值类型,可以省去长度这个字段?
  //value
  WriteInt32NoTag(value, output);
}

//可变字长类型,这里是string
void WireFormatLite::WriteString(int field_number, const string& value, io::CodedOutputStream* output) {
  // String is for UTF-8 text only
  //tag
  WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
  //length,这里长度是采用varint编码方式,可以省不少字节
  GOOGLE_CHECK(value.size() <= kint32max);
  output->WriteVarint32(value.size());
  //value
  output->WriteString(value);
}

//嵌套的message字段
void WireFormatLite::WriteMessage(int field_number, const MessageLite& value, io::CodedOutputStream* output) {
  //tag
  WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
  //length, 这里计算message的长度,然后再写出。
  const int size = value.GetCachedSize();
  output->WriteVarint32(size);
  //value,这里的value是message类型,可以看做是在递归进行序列化
  value.SerializeWithCachedSizes(output);
}

inline void WireFormatLite::WriteTag(int field_number, WireType type, io::CodedOutputStream* output) {
  output->WriteTag(MakeTag(field_number, type));
}

inline uint32 WireFormatLite::MakeTag(int field_number, WireType type) {
  return GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(field_number, type);
}

//这有个宏很关键,正好印证上面提到的计算key值的方式
#define GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(FIELD_NUMBER, TYPE)                  \
  static_cast<uint32>(                                                   \
    ((FIELD_NUMBER) << ::google::protobuf::internal::WireFormatLite::kTagTypeBits) \
      | (TYPE))

// Number of bits in a tag which identify the wire type.
static const int kTagTypeBits = 3;

3.3反序列化

??了解了上面的序列化过程之后再看反序列化过程就简单多了。从本质上来说就是从一个输入流里,依次读取tag值,然后判断它是那种wire_type类型的数据,再调用对应的方法读取对应的值。整个处理过程在一个while循环中,直到数据处理完毕才终止。
??这里直接上代码,这个解析过程是针对特定的message产生的。如果message里有多个字段,这里的case分支也会越来越多,GetTagFieldNumber(tag)方法会解析出接下来的数据是第几个字段,然后根据.proto中定义的数据类型进行判断,如果wire_type值正确则调用对应的读取数据的方法。

bool RspBody::MergePartialFromCodedStream(::google::protobuf::io::CodedInputStream* input) {
#define DO_(EXPRESSION) if (!(EXPRESSION)) return false
    ::google::protobuf::uint32 tag;
    while ((tag = input->ReadTag()) != 0) {
        switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
            //optional message msg_set_req = 1;
            case 1: {
                if (::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
                    ::google::protobuf::internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED) {
                    DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(input, mutable_msg_set_rsp()));
                } else {
                    goto handle_uninterpreted;
                }
                if (input->ExpectAtEnd()) return true;
                break;
            }
            default: {
            handle_uninterpreted:
                if (::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
                    ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
                    return true;
                }
                DO_(::google::protobuf::internal::WireFormatLite::SkipField(input, tag));
                break;
            }
        }
    }
    return true;
#undef DO_
}

四、高级进阶 - PbCodec

有待更新… = =。看我心情吧哈哈哈哈哈

参考资料

  1. Google protocol buffer - Language Guide
  2. GoogleProtocolBuffer的使用和原理
  3. 鸣谢armingli大大的文章【Protobuf组包解包新用法之PbCodec篇】

Google protocol buffer 使用和原理浅析 - 附带进阶使用方式

标签:

原文地址:http://blog.csdn.net/linyousong/article/details/51759150

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!