Protobuf是Google开发一种数据描述语言,能够将结构化数据序列化,可用于数据存储、通信协议等方面。据Google官方文档介绍,现在Google内部已经有48,162个消息类型定义在12,183个proto文件中。
简介
protobuf是Google开发的一种数据描述语言语言,能够将结构化的数据序列化,可用于数据存储,通信协议等方面,官方版本支持C++,Java,Python,社区版本支持更多语言。
比如说程序中生成了一个链表,但是程序退出重启后,还要重新生成链表,有时候,我们很需要上次程序中该链表中记录的数据。这些数据或许是经过很多大量运算生成的,每次都重新生成这些数据的话,需要消耗大量时间。这时候就可以考虑使用protobuf,将其序列化后保存在文件中,下次使用的时候,加载文件,反序列化后就可以直接使用了。
该项目在github的官方地址
值得注意的是,protobuf是以二进制来存储数据的。相对于JSON和XML具有以下优点:
简洁
体积小:消息大小只需要XML的1/10 ~ 1/3
速度快:解析速度比XML快20 ~ 100倍
使用Protobuf的编译器,可以生成更容易在编程中使用的数据访问代码
更好的兼容性,Protobuf设计的一个原则就是要能够很好的支持向下或向上兼容
protobuf开发环境
主要介绍c++,Java开发环境,使用的版本protobuf 2.3.0。因为Android5.1.1源码中使用的就是该版本,为了能快速编译出Android能使用的so库,所以选择这个版本。如果喜欢最新版,可以从上面的github官网下载。
ubuntu :
./configuer --prefix=指定安装路径(可以不指定)
make
make install (可能需要sudo权限)
会编译出一个protoc的可执行程序,以及开发所需的头文件和库。这个可执行程序,要经常使用,用来将proto协议文件,转换为头文件和对应的代码。
java:
点此进入jar包下载页面,选择2.3.0版本。供java程序调用。
Android native:
Android 源码/external/protobuf
直接:
mmm external/protobuf
会编译出j出很多jar,和c++静态库。jar,推荐还是选择从上面下载的,静态库使用不是很方便。这里修改Android.mk编译so库版本。
# C++ full library - libcxx version
# =======================================================
include $(CLEAR_VARS)
LOCAL_MODULE := libprotobuf-cpp-2.3.0-full-libcxx-rtti
LOCAL_MODULE_TAGS := optional
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := $(protobuf_cc_full_src_files)
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/android \
external/zlib \
$(LOCAL_PATH)/src
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)
LOCAL_CFLAGS := -frtti $(IGNORED_WARNINGS)
LOCAL_CPPFLAGS := -w
LOCAL_SHARED_LIBRARIES := libz
include external/libcxx/libcxx.mk
include $(BUILD_SHARED_LIBRARY)
#include $(BUILD_STATIC_JAVA_LIBRARY)
# Clean temp vars
protobuf_cc_full_src_files :=
这里还用到了libcxx库,该库实现了c++11很多特性,开发Android Native程序时,我经常使用到这个库,所以这里也用该库来编译protobuf了。
成功后,如下所示:
Install: out/target/product/shamu/system/lib/libprotobuf-cpp-2.3.0-full-libcxx-rtti.so
快速使用
主要是介绍如何使用,前面在PC主机中编译出来的protoc可执行程序,将proto协议文件转换为对应的代码,也顺便看下proto协议文件长什么样子。
如下片段,摘自protobuf源码中的example中的addressbook.proto
message Person {
required string name = 1;
required int32 id = 2; // Unique ID number for this person.
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
使用protoc命令生成代码,使用cpp_out、java_out、python_out命令选项可以生成C++、Java、Python代码。例如:
protoc addressbook.proto -I. --cpp_out=.
上述命令执行后,产生两个用于c++新文件:
addressbook.pb.cc
addressbook.pb.h
可以将cc改为cpp.之后在自己的工程中包含这两个文件就可以按照下面所示使用proto中定义的数据协议了:
这两个文件可以大致粗略的看看,因为可以从中知道定义哪些可用的调用方法等,一看便知怎么用。
proto文件的语法
在消息定义中,需要明确以下三点:
确定消息命名,给消息取一个有意义的名字。
指定字段的类型
定义字段的编号,在Protocol Buffers中,字段的编号非常重要,字段名仅仅是作为参考和生成代码用。需要注意的是字段的编号区间范围,其中19000 ~ 19999被Protocol Buffers作为保留字段。
先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
SearchRequest就是消息的名字,该消息有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个修饰符,一种类型,一个名字和一个编号。
所指定的字段类型修饰符必须是如下之一:
required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的;
optional:消息格式中该字段可以有0个或1个值(不超过1个),也就是可有可无;
repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
其中数据类型和c++,java的对应如下:
分配编号:
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
消息也是可以嵌套的,即message套message,还可以使用枚举。还可以使用import导入其他proto文件。
包(Package)
通常都要为.proto文件增加一个package声明符,用来防止不同的消息类型有命名冲突。
package foo.bar;
message Open { ... }
在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:
message Foo {
...
required foo.bar.Open open = 1;
...
}
包的声明符会根据使用语言的不同影响生成的代码。
对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中;
对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;
对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
protobuf使用流程
编写协议文件,也就是.proto文件
利用protoc命令将协议文件转换为我们需要的开发语言接口.该接口中包含了对协议中定义数据的操作方法.可以从生成的.h文件中查看有哪些接口,常用的就是设置字段数据,获取字段数据,序列化,反序列化等;
列举一些公用方法:
isInitialized(): 检查是否所有的required字段是否被赋值
toString(): 返回一个便于阅读的message表示(本来是二进制的,不可读)
byte[] toByteArray();: 序列化message并且返回一个原始字节类型的字节数组
static XXX parseFrom(byte[] data);: 将给定的字节数组解析为message
void writeTo(OutputStream output);: 将序列化后的message写入到输出流
static Person parseFrom(InputStream input);: 读入并且将输入流解析为一个message
在项目中使用生成的接口操作协议数据;
协议中每个message都会转换为一个类,使用的时候创建一个消息对象,初始化里面的属性值并且序列化,然后可以存储到文件,可以socket发送等等.
使用数据的时候,先反序列化,然后就可以正常使用了.
c++编译如下,注意加pthread库,否则出错.
g++ xx.cpp xx.pb.cc -I /usr/local/protobuf/include -L /usr/local/protobuf/lib -lprotobuf -pthread
protobuf编码协议
搞清楚编码协议,是为了能清楚数据大小,这样才调试socket程序的时候,能看懂数据长度的含义.
在Protobuf中采用Base128变长编码,所谓变长编码是和定长编码相对的,定长编码使用固定字节数来表示,如int32类型的数字固定使用4 bytes表示,而变长编码是需要几个字节就使用几个字节,如对于int32类型的数字1来说,只需要1 bytes足够。Base128变长编码的原则就两条
每个字节使用低7位表示数字,除了最后一个字节,其他字节的最高位都设置为1。
采用LittleEndian字节序
test.proto
message Book{
required uint32 money = 1;
}
编译proto文件:
protoc test.proto -I. --cpp_out=.
main.cpp
#include "test.pb.h"
#include <stdio.h>
int main(){
Book book;
book.set_money(150);
int size = book.ByteSize();
unsigned char bts[size];
book.SerializeToArray(bts, size);
for(int i=0;i<size;i++)
printf("0x%x \n",bts[i]);
return 0;
}
g++ main.cpp test.pb.cpp -I ~/mylib/protobuf/include -L ~/mylib/protobuf/lib -lprotobuf -o test -lpthread
运行test:
0x8
0x96
0x1
一个Protobuf的消息包含一系列字段key/value,每个字段由一个变长32位整数作为字段头,后面跟随字段体。字段头,也就是key的格式如下:
(field_number << 3) | wire_type
field_number: 字段序号
wire_type: 字段编码类型
字段编码类型如下:
字段序号,就是在定义message中的字段的时候添加的标号.
再看一个例子:
看嵌套:
message Test1{
requaried int32 a = 1;
}
message Test3{
requaried Test1 c = 3;
}
负数编码:
采用ZigZag Encoding