这是对protobuf 在项目中的应用和日常学习的总结,记录一些重点。持续完善勘正。
项目应用:
- 存储。使用 protobuf 序列化,之后 mysql 持久化到库表的 blob 字段;
- 消息传递。客户端与服务器、服务器与服务器间通讯,将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;
- 反序列化流程:
- 动态解析
8. 优势:
- 兼容性好,比如服务器版本下proto文件新增字段,但是客户端仍然是旧版本,客户端接收到数据后,发现找不到新字段的tag,于是会跳过这个tag指向的新增字段相应的字节数;
- 体积小,使用varint编码方式,值越小需要的空间越小,例如int32,一般情况下需要4个字节,但是使用varint编码,值较小的数字可能只需要1个字节,对于负数,有sint32,使用zigzag编码;消息中某个字段如果没有值,它是不占空间的;
- 效率高,例如xml需要解析doom树,很复杂,而protobuf只需要将二进制序列做位移计算处理即可,无需解析器;
9. proto3:
取消了option,required 修饰符
complier不生成has_xxx