Protobuf Summary(一) 概览

这是对protobuf 在项目中的应用和日常学习的总结,记录一些重点。持续完善勘正。

项目应用:

  1. 存储。使用 protobuf 序列化,之后 mysql 持久化到库表的 blob 字段;
  2. 消息传递。客户端与服务器、服务器与服务器间通讯,将proto消息序列化到原始struct消息变长字段中。

相关知识:

1. 编译配置CMake

  • 生成pb.h, pb.cc ,省略了一些变量赋值过程
    #使用 protobuf_generate_cpp
    file(GLOB_RECURSE PROTOS message.proto)
    find_package(Protobuf REQUIRED)
    protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTOS})
    
    #或者使用protoc
    add_custom_command(
    set(PB_H message.pb.h)
    set(PB_CC message.pb.cc)
    OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/${PB_H} ${CMAKE_CURRENT_SOURCE_DIR}/${PB_CC}
    COMMAND protoc -I ${CMAKE_CURRENT_SOURCE_DIR} --proto_path=${PB_PATH} --cpp_out=${CMAKE_CURRENT_SOURCE_DIR} message.proto
    DEPENDS  message.proto
    )

--cpp_out: c++代码生成目录,--proto_path= 或者 -I 指定的目录(proto文件搜索目录,如果没有提供则会在complier被调用的目录搜索,所以最好设置成项目根目录)将会被cpp_out取代,例如:

protoc --proto_path=src --cpp_out=build/gen src/foo.proto src/bar/baz.proto

将src/foo.proto src/bar/baz.proto  生成 build/gen/foo.pb.h, build/gen/foo.pb.cc, build/gen/bar/baz.pb.h, build/gen/bar/baz.pb.cc, complier 可以创建build/gen/bar目录,但是不会创建build/gen目录。

  • 链接库
    add_executable(PBTest ProtoTest.cpp ${PROTO_SRCS})
    target_link_libraries(PBTest ${Protobuf_LIBRARIES})

2. Message:

  • 在proto文件中,我们有三种生成proto访问类的选项:
     

    option optimize_for = SPEED; (继承 google::protobuf::Message,以性能最高的方式实现了所有的函数)
    option optimize_for = CODE_SIZE; (override了一些必要的函数,其他的依赖于反射机制,此选项生成的代码体积小,但是性能也有所降低)
    option optimize_for = LITE_RUNTIME; (继承google::protobuf::MessageLite, 它是Message的基类,只实现了部分高效的函数,另外链接libprotobuf-lite.so, 而不是libprotobuf.so,生成的代码体积小,比较适合移动设备。

  • 常用接口

    bool ParseFromString(const string& data)  从序列化中的二进制字符串中解析消息

    bool SerializeToString(string* output) const  将message序列化到给定的字符串中

    static const Descriptor* descriptor()  返回该message的描述符(Descriptor),描述符包含了字段信息

    static const Foo& default_instance()  返回类似newly-constructed实例,default instance 可以被当作工厂使用New方法

3. Filelds: 

  • 对于filed accessor(字段访问器)的函数,返回常引用同时间,如果有对message的其他改动,这个常引用有可能失效。
  • 对于返回的指针,使用message的函数操作任何field,都有可能造成指针失效。
  • google::protobuf::Map高效的插入方法是使用下标map[key] = value, 使用insert方法将隐式地使用深拷贝

    map 相当于是key/value pair的map entry,并且它是repeated的,如下图。 
    message MapFieldEntry {
      optional key_type key = 1;
      optional value_type value = 2;
    }
    
    repeated MapFieldEntry map_field = N;

     

  • string类型的函数,set_allocated_xxx(string* value),获得value的所有权,相当于管理value指针,可以直接修改内容,

    release_xxx()释放所有权,string field还原为默认值。
  • 对于proto2中repeated类型, 例如 repeated int32 samples = 4 [packed=true], packed置为true,则使用更紧凑的编码方式,并且没有负面影响,

4. Service: 

  • proto文件中 option cc_generic_services = true;  complier会生成基于服务的代码,这些代码需要绑定到特定的rpc system
  • Interface
    service Foo {
      rpc Bar(FooRequest) returns(FooResponse);
    }
    virtual void Bar(RpcController* controller, const FooRequest* request, FooResponse* response, Closure* done);

Foo子类化Service Interface,实现了GetDescriptor ,CallMethod,GetRequestPrototype,GetResponsePrototype 。

  • stab

    对上图的Service Foo,complier会生成相应的Foo_Stub,类似客户端,可以在指定的channel上向服务器发送请求消息,
    例如构造函数 Foo_Stub(RpcChannel* channel) ,在指定的channel上创建stub
     

5. Arena Allocation:

  • 默认情况下,protobuf为每个消息对象、其子对象、string会申请堆内存。这些内存分配发生在在解析消息和在内存中构建消息时,当对象和它的子对象释放时,会进行关联的内存释放。 
  • cc_enable_arenas 开启Arena Allocation, 优化内存使用,提高性能。这时,新的对象将会从 被称为Arena的预分配的内存中申请, 避免了每次构造时new,delete内存, 最后将在释放Arena时一起释放。
    #include <google/protobuf/arena.h>
    {
      google::protobuf::Arena arena;
      MyMessage* message = google::protobuf::Arena::CreateMessage<MyMessage>(&arena);
      // ...
    }

    MyMessage 以及它的repeated 字段将在arena上申请,另外我们不能手动delete返回的message指针。
    Arena 的CreateMessage函数时线程安全的,但是Reset(释放arena内存,执行arena list的析构函数)不是线程安全的。
    如果父消息支持Arena,子消息不支持Arena,那么子消息将申请堆内存,放入父消息的Arena List中,这样生命周期都将与Arena内存相关联。

6. Self-describing Messages(自描述信息):

  •  protobuf 消息没有包含它自己的类型信息。只有原始信息而没有提供一个proto结构时,解析消息是无能为力的。

     但是proto文件内容本身可以用protobuf表示,调用protoc生成文件时设置--descriptor_set_out, 定义自描述信息:
    import "google/protobuf/any.proto";
    import "google/protobuf/descriptor.proto";
    
    message SelfDescribingMessage {
      // Set of FileDescriptorProtos which describe the type and its dependencies.
      google.protobuf.FileDescriptorSet descriptor_set = 1;
    
      // The message and its type, encoded as an Any message.
      google.protobuf.Any message = 2;
    }

    在Descriptor.proto中有一些消息定义,以下为举例
    ① 文件描述集合 FileDescriptorSet ,可以表示一组proto文件:

    // The protocol compiler can output a FileDescriptorSet containing the .proto
    // files it parses.
    message FileDescriptorSet {
      repeated FileDescriptorProto file = 1;
    }

    ② 文件描述 FileDescriptorProto,表示单个proto文件

    // Describes a complete .proto file.
    message FileDescriptorProto {
        // ...省略  可参考官网
    }

    ③ 消息描述 DescriptorProto, 表示单个message

    // Describes a message type.
    message DescriptorProto {
        // 省略
    }

    ④ 字段描述 FieldDescriptorProto , 表示单个field

    // Describes a field within a message.
    message FieldDescriptorProto {
        // 省略
    }

     

7. 序列化:

  • proto序列化后为二进制流,形式是:Tag-Value 或者 Tag-Length-Value,其中 Tag = (field_number << 3) | wire_type ,filed_num指字段序号,wire_type根据编码方式确定,例如varint,wire_type=0,采取Tag-Value;  repleated 或者map(也是repeated) wire_type=2,采取Tag-Length-Value;
  • 反序列化流程:
图 9. 解包流程图
图片来源:参考资料4
  • 动态解析

8. 优势:

  • 兼容性好,比如服务器版本下proto文件新增字段,但是客户端仍然是旧版本,客户端接收到数据后,发现找不到新字段的tag,于是会跳过这个tag指向的新增字段相应的字节数;
  • 体积小,使用varint编码方式,值越小需要的空间越小,例如int32,一般情况下需要4个字节,但是使用varint编码,值较小的数字可能只需要1个字节,对于负数,有sint32,使用zigzag编码;消息中某个字段如果没有值,它是不占空间的;
  • 效率高,例如xml需要解析doom树,很复杂,而protobuf只需要将二进制序列做位移计算处理即可,无需解析器;

9. proto3: 

取消了option,required 修饰符

complier不生成has_xxx

参考资料:

  1. https://developers.google.com/protocol-buffers/docs/proto  官方文档
  2. https://blog.csdn.net/larry_zeng1/article/details/82530810   
  3. https://www.jianshu.com/p/417f8f734470  编码
  4. https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/   动态编译、编码、传输
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值