Protobuf简单使用

from:Protobuf简单使用

Protobuf是Google开发一种数据描述语言,能够将结构化数据序列化,可用于数据存储、通信协议等方面。据Google官方文档介绍,现在Google内部已经有48,162个消息类型定义在12,183个proto文件中。

简介

protobuf是Google开发的一种数据描述语言语言,能够将结构化的数据序列化,可用于数据存储,通信协议等方面,官方版本支持C++,Java,Python,社区版本支持更多语言。

比如说程序中生成了一个链表,但是程序退出重启后,还要重新生成链表,有时候,我们很需要上次程序中该链表中记录的数据。这些数据或许是经过很多大量运算生成的,每次都重新生成这些数据的话,需要消耗大量时间。这时候就可以考虑使用protobuf,将其序列化后保存在文件中,下次使用的时候,加载文件,反序列化后就可以直接使用了。

该项目在github的官方地址

值得注意的是,protobuf是以二进制来存储数据的。相对于JSON和XML具有以下优点:

  1. 简洁
  2. 体积小:消息大小只需要XML的1/10 ~ 1/3
  3. 速度快:解析速度比XML快20 ~ 100倍
  4. 使用Protobuf的编译器,可以生成更容易在编程中使用的数据访问代码
  5. 更好的兼容性,Protobuf设计的一个原则就是要能够很好的支持向下或向上兼容

protobuf开发环境

主要介绍c++,Java开发环境,使用的版本protobuf 2.3.0。因为Android5.1.1源码中使用的就是该版本,为了能快速编译出Android能使用的so库,所以选择这个版本。如果喜欢最新版,可以从上面的github官网下载。

点此下载protobuf 2.3.0版本源码

ubuntu :
1
2
3
4
5
./configuer --prefix=指定安装路径(可以不指定)

make 

make install (可能需要sudo权限)

会编译出一个protoc的可执行程序,以及开发所需的头文件和库。这个可执行程序,要经常使用,用来将proto协议文件,转换为头文件和对应的代码。

java:

点此进入jar包下载页面,选择2.3.0版本。供java程序调用。

Android native:

Android 源码/external/protobuf

直接:

1
mmm external/protobuf

会编译出j出很多jar,和c++静态库。jar,推荐还是选择从上面下载的,静态库使用不是很方便。这里修改Android.mk编译so库版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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了。

成功后,如下所示:

1
Install: out/target/product/shamu/system/lib/libprotobuf-cpp-2.3.0-full-libcxx-rtti.so
快速使用

主要是介绍如何使用,前面在PC主机中编译出来的protoc可执行程序,将proto协议文件转换为对应的代码,也顺便看下proto协议文件长什么样子。

如下片段,摘自protobuf源码中的example中的addressbook.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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代码。例如:

1
protoc addressbook.proto -I. --cpp_out=.

上述命令执行后,产生两个用于c++新文件:

1
2
addressbook.pb.cc
addressbook.pb.h

可以将cc改为cpp.之后在自己的工程中包含这两个文件就可以按照下面所示使用proto中定义的数据协议了:

这两个文件可以大致粗略的看看,因为可以从中知道定义哪些可用的调用方法等,一看便知怎么用。

proto文件的语法

在消息定义中,需要明确以下三点:

  1. 确定消息命名,给消息取一个有意义的名字。
  2. 指定字段的类型
  3. 定义字段的编号,在Protocol Buffers中,字段的编号非常重要,字段名仅仅是作为参考和生成代码用。需要注意的是字段的编号区间范围,其中19000 ~ 19999被Protocol Buffers作为保留字段。

先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:

1
2
3
4
5
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声明符,用来防止不同的消息类型有命名冲突。

1
2
package foo.bar;
message Open { ... }

在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:

1
2
3
4
5
message Foo {
  ...
  required foo.bar.Open open = 1;
  ...
}

包的声明符会根据使用语言的不同影响生成的代码。

对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中;

对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;

对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。

protobuf使用流程

  1. 编写协议文件,也就是.proto文件
  2. 利用protoc命令将协议文件转换为我们需要的开发语言接口.该接口中包含了对协议中定义数据的操作方法.可以从生成的.h文件中查看有哪些接口,常用的就是设置字段数据,获取字段数据,序列化,反序列化等;

    列举一些公用方法:

    1
    2
    3
    4
    5
    6
    isInitialized(): 检查是否所有的required字段是否被赋值
    toString(): 返回一个便于阅读的message表示(本来是二进制的,不可读)
    byte[] toByteArray();: 序列化message并且返回一个原始字节类型的字节数组
    static XXX parseFrom(byte[] data);: 将给定的字节数组解析为message
    void writeTo(OutputStream output);: 将序列化后的message写入到输出流
    static Person parseFrom(InputStream input);: 读入并且将输入流解析为一个message
  1. 在项目中使用生成的接口操作协议数据;

    协议中每个message都会转换为一个类,使用的时候创建一个消息对象,初始化里面的属性值并且序列化,然后可以存储到文件,可以socket发送等等.

    使用数据的时候,先反序列化,然后就可以正常使用了.

  1. c++编译如下,注意加pthread库,否则出错.
1
g++  xx.cpp xx.pb.cc  -I /usr/local/protobuf/include -L /usr/local/protobuf/lib -lprotobuf -pthread

protobuf编码协议

搞清楚编码协议,是为了能清楚数据大小,这样才调试socket程序的时候,能看懂数据长度的含义.

在Protobuf中采用Base­128变长编码,所谓变长编码是和定长编码相对的,定长编码使用固定字节数来表示,如int32类型的数字固定使用4 bytes表示,而变长编码是需要几个字节就使用几个字节,如对于int32类型的数字1来说,只需要1 bytes足够。Base­128变长编码的原则就两条

  1. 每个字节使用低7位表示数字,除了最后一个字节,其他字节的最高位都设置为1。
  2. 采用Little­Endian字节序

test.proto

1
2
3
message Book{
    required uint32 money = 1;
}

编译proto文件:

1
protoc test.proto -I. --cpp_out=.

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}
1
g++ main.cpp test.pb.cpp -I ~/mylib/protobuf/include -L ~/mylib/protobuf/lib -lprotobuf -o test -lpthread

运行test:

1
2
3
0x8 
0x96 
0x1

一个Protobuf的消息包含一系列字段key/value,每个字段由一个变长32位整数作为字段头,后面跟随字段体。字段头,也就是key的格式如下:

1
2
3
(field_number << 3) | wire_type
­field_number:   字段序号 
­wire_type:  字段编码类型

字段编码类型如下:

字段序号,就是在定义message中的字段的时候添加的标号.

再看一个例子:

看嵌套:

1
message Test1{
requaried int32 a = 1;
}
message Test3{
requaried Test1 c = 3;
}

负数编码:

采用ZigZag Encoding

此条目发表在google分类目录,贴了标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注