Protobuf 数据交换格式的使用方法

现今,如果问什么格式的数据交互最红火,非Google家的protobuf莫属了。相比XML、Json,其有点就是使用接口简单、序列化和解析速度快、数据小传输效率高,同时其还具有向后兼容、跨平台、以及丰富的语言支持接口(居然js都支持,看来直逼Json了啊)的优势。当然,其缺点是不像XML、Json那种可读性和自解释性强,特别是在网络通信时候调试起来比较麻烦。

翻墙照着Google的文档,把protobuf走了一遍,总体感觉不愧是大厂的作品,考虑到的是效率、多语言支持、兼容性以及分布式系统中多版本的兼容和演进,同时其内部是使用C++实现的,在proto“语法”设计上也会让C++用户感觉十分亲切。

老习惯还是顺便做个笔记吧。

一、从例子说起

protobuf的使用,需要事先写好.proto文件,这个文件规定了数据传输和接受方交换数据的字段、类型等信息。然后使用编译器protobuf-compiler编译这个文件,就可以产生指定语言类型所需的辅助性文件(比如C++的.h和.cc,然后对每一个message都会产生一个类进行描述)了,然后方便的集成到项目代码中使用,当然除了命令行模式的编译,也可以在源代码中读取.proto文件进行动态编译。

1.1 .proto文件的格式

messagePerson{requiredstringname =1;// Your namerequiredint32id =2;// Your IDoptionalstringemail =3;enumPhoneType{optionallow_alias =true; MOBILE = 0; CELLPHONE = 0; HOME = 1; WORK = 2; }messagePhoneNumber{requiredstringnumber =1;optionalPhoneType type =2[default = HOME]; }repeatedPhoneNumber phone =4; reserved 2,15,9to11;// reserved保留 reserved "foo","bar";}

(1) 每个字段前面都必须有required、optional、repeated,分别表示后面的字段出现1次、0或1次、0或多次,对于repeated字段,其相同类型字段多个值出现的顺序会被保留。

(2) 数据类型可以是整形、string类型(支持的类型主要有float、double、[s|u| ]int[32|64]、bool、string),同时还支持自定义类型的嵌套。每一个字段后面都有一个唯一的数字标号(number tag),为了提高效率,1-15是用一个字节编码,16-2047是用的两个字节编码,所以根据霍夫曼编码的愿意应该把常用的字段用小于15的数字标号。

(3) 同一个.proto文件中可以定义多个message,尤其当他们在业务上逻辑相关时候更应该这样。.proto文件的注释支持用C/C++的//风格注释。

(4) 后续更新的时候,可能某些字段不想要了。为了兼容性起见,不应当仅仅注释或者删除掉,而应该用reserved字段尤其对数字标号进行保留,防止被别人再次使用,然后让老的程序错误解析这些字段。

(5) 对于optional的字段,可以使用default提供对应的默认值,这样当message解析发现没有这个字段时候,那么就会:如果设定有默认值,就使用这个默认值;否则其值是类型相关的——0、false、空string、枚举的第一个元素。

(6) enum枚举类型有一个选项allow_alias,打开它的时候,允许同一个枚举值有多个枚举的名字(比如上文的MOBILE和CELLPHONE)。

1.2 .proto文件其它相关

(1) 导入定义

import可以将别的文件的定义导入到当前文件,默认的import行为是只能使用导入文件中的直接定义,如果需要嵌套使用导入文件的内容,达到类似递归的应用效果,可以使用import public语句。

// old.protoimport public"new.proto";import "other.proto";// client.protoimport "old.proto";//此时可以使用old.proto和new.proto中的内容,但是看不到other.proto内容

protobuf-compiler编译器对.proto文件搜索路径默认是执行protoc的当前路径,当然命令行可以使用-I/–proto_path来添加搜索路径。

(2) 嵌套类型

可以使用Parent.Type这种语法来实现嵌message的使用,而且不限制嵌套的层次深度

message SearchResponse { message Result { required stringurl =1; optional stringtitle =2; } repeated Result result = 1;}message SomeOtherMessage { optional SearchResponse.Result result = 1;}

(3) 更新消息类型

从整个文档看来,Google对于这种向后兼容性看的很重要,这也是一个大型系统逐渐演进所必需的。如果要修改一个.proto文件,那么需要遵守以下的一些守则和约定:

a. 对于一个已经存在的字段不要修改其数字标号;

b. 新增加的字段应该是optional或者repeated的;

c. 不再使用的字段,可以用OBSOLETE_等前缀命名表示废弃,但是绝对不要重用其数字标号,记得使用上面的reserved对这些数字标号保护起来;

d. 非required可以转为extension(保留给第三方在他们自己的.proto文件中使用);

e. int32、uint32、int64、uint64、bool是兼容的,客户端可以进行结果的强制转换;

f. 修改default默认值是允许的,因为默认值不会真正的传输,只跟程序使用的.proto文件有关;

(4) extension

上面的方式可以把特定的数字标号区域保留给扩展,扩展在自己的.proto文件中,先import原有的.proto,然后再次打开message,添加扩充自己的字段,比如

// foo.proto
message Foo {
// ...
extensions 100to199; }

// your.proto
import foo.proto;
extend Foo { optional int32 bar = 126; }

但是跟前面基本字段不同,extension的访问需要使用特殊的接口来操作,比如设置值,需要调用

foo.SetExtension(bar, 15);
其它这类操作接口包括:HasExtension()、ClearExtension()、GetExtension()、MutableExtension()、AddExtension()。

(5) oneof
类似于C/C++中的union类型,当oneof包围多个可选字段的时候,至多只能给一个字段赋值,当给某个字段设置的时候,会自动清除已存其它字段的值,因为这些字段都是共享同一内存的,为的就是节省内存。

message SampleMessage { oneof test_oneof {stringname =4; SubMessage sub_message = 9; }}SampleMessage message;message.set_name("name");CHECK(message.has_name()); oneof中的字段定义不能有require、optional、repeated,然后具体使用的时候可以当作optional一样来使用了。还有,如果一个message中有多个同名的oneof,只有最后一个可见的会被实际使用;extensions不支持oneof;oneof不能被repeated;C++中支持swap两个oneof,交换后可访问字段变成互为对方的那个。(6) mapsproto支持相关性容器map,其定义的格式是map map_field = N;其中的key_type除了不能是浮点和bytes,其它类型(整形、string)都可以作为key;针对map其wire format的排序和迭代顺序是未定义的,用户不应当依赖其顺序;当将其转成文本类型的时候,是按照整形从小到大或者字符串的升序来排序的;当解析或者合并map的时候,如果有重复的key,那么只有最后看见的那个key被使用,而当从文本中解析的时候,如果有重复的key会做报错处理。(7) Packagesproto的名字查找类似于C++,从最内层的类依次向外查找。有时候为了防止名字冲突,可以在.proto文件的开头声明package,起到类似名字空间的效果(实际上产生C++辅助代码的时候就是放到对应的namespace中的,比如foo.bar生成了foo::bar)。package foo.bar; //namespace foo::barmessage Open { ... }// 另外一个proto中使用message Foo { required foo.bar.Open open = 1;}## 1.3 将上面的例子用起来针对上面的.proto文件,使用protoc编译,可以根据语言产生对应的源代码文件(比如C++的.h和.cc)。实测发现,当前很多发行版还是默认打包的protobuf-compiler-2.6甚至更旧的版本,所以建议在GitHub上面下载源代码自己编译安装最新的3.0版本。然后,3.0的版本今年才正式发布的,语法跟之前稳定版2.6差异还是挺大的,总体的感觉是让protobuf使用更简洁了。具体的修改日志可以看参考列表中的Release Note,包括:不再区分optional、required,默认都是optional;需要指定syntax版本,默认是proto2;不支持default默认值等。编译的格式,和上面实际用到的编译命令是:protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR path/to/file.proto➜ ~ protoc --cpp_out=./ msg.proto如果DST_DIR使用.zip结尾,那么产生的文件会自动用.zip打包,同时输入的.proto文件可以一次指定一个或者多个。编译结束后会生成msg.pb.cc和msg.pb.h两个文件,然后就可以轻松应用了!# include# includeusingnamespacestd;# include"msg.pb.h"intmain(intargc,char* argv[]){ Person person; person.set_name("Nicol TAO"); person.set_id(1234); person.set_email("taozhijiang@126.com");fstream output("myfile", ios::out | ios::binary); person.SerializeToOstream(&output); output.close();fstream input("myfile", ios::in | ios::binary); Person person2; person2.ParseFromIstream(&input);coutint32 foo() const;voidset_foo(int32 value);oid clear_foo();//清除,下次调用foo()会返回0(2) Singular单个字符串类型stringfoo =1;bytes foo = 1;===>conststring& foo()const;voidset_foo(conststring& value);voidset_foo(constchar* value);voidset_foo(constchar* value,intsize);string*mutable_foo();//返回一个可以修改string值的指针voidclear_foo();voidset_allocated_foo(string* value);//吧value设置到foo,如果foo之前有string,则释放掉之前的string//如果value==NULL,则等同于clear_foo()string*release_foo();//调用后foo释放string的控制权(3) Singular嵌入message类型message Bar {}Bar foo = 1;===>boolhas_foo();//检查foo是否已经setconstBar&foo();//返回值,如果没有set,就返回一个没有设置的Bar,Bar::default_instance()Bar* mutable_foo();//返回mutable指针,如果没有set,内部就会newly-allocated Bar并返回voidclear_foo();voidset_allocated_foo(Bar* bar);//如果bar==NULL,等同于clear_foo()Bar* release_foo();(4) Repeated数字类型repeated int32 foo = 1;===>intfoo_size()const;//元素的个数int32 foo(intindex)const;//0-based索引对应的值voidset_foo(intindex, int32 value);voidadd_foo(int32 value);voidclear_foo();//清除所有的元素对于枚举、字符串、嵌入message,也有对应的repeated版本,可以参阅其手册,很容易理解和想到。(5) Oneof数字类型oneof oneof_name { int32 foo = 1; ... }===>boolhas_foo()const;//检查当前oneof类型是kFooint32 foo()const;//如果当前是kFoo,返回其值,否则返回0voidset_foo(int32 value);//如果当前不是kFoo,调用clear_oneof_name(),然后设置其值,并且oneof_name_case()会返回kFoovoidclear_foo();//如果当前不是kFoo,则什么也不做;否则,清除其值,然后has_foo()==false,foo()==0,同时oneof_name_case()返回ONEOF_NAME_NOT_SET。对于枚举、字符串、嵌入message,也有对应的Oneof版本,可以参阅其手册,很容易理解和想到。(6) Map类型map weight =1;===>constgoogle::protobuf::Map& weight();google::protobuf::Map* mutable_weight();上面的weight()和mutable_weight()会得到可修改和不可修改两个map,其可以支持std::map和std::unorderd_map中常用的函数接口,包括:迭代器、元素访问、查找、修改(添加和删除)、拷贝等。//这种插入会被insert要好,免除可能的元素深度拷贝(*my_enclosing_proto->mutable_weight())[my_key] = my_value;std::map standard_map(message.weight().begin(), message.weight().end());同时如上面所示,如果不想用protobuf::Map的接口,可以像上面一样创建标准的std::map,不过构造这个map会产生所有元素的深度拷贝。本文完!#Protobufs、c++、产品经理#

版权声明

本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部