protobuf示例与编码详解

原创
01/04 19:29
阅读数 4.1K

1. 简介

2. 定义message

首先,我们通过一个简单的实例来看一下怎样定义message。

syntax = "proto2";

option java_package = "vip.mycollege.netty.proto";
option java_outer_classname = "BookProto";

//书籍
message Book {
	optional uint32 id = 1;//书籍id 
	optional string name = 2;//书籍名称
	repeated BookComment bookComments = 3;//书籍评论

}

//评论
message BookComment {
	optional uint64 id = 1;//评论id
	optional uint32 bookId = 2;//书籍id
	optional string content = 5;//评论内容
}

上面是一个非常简单的message定义:

第一行,基本固定:syntax = "proto2"或syntax = "proto3",选择要使用protobuf的版本。

java_package指定生成的类要放在那个包下。

每一个proto文件会生成一个类,java_outer_classname用来指定生成的这个类的最外层的类名,如果没有会根据文件名称生成,下划线分割,驼峰法。

message字段定义说明:

optional   unit64     id       = 3            [defalut = MALE];
字段规则  字段类型  字段名称   字段唯一标识符    可选的选项:默认值

其中字段规则:

字段规则 说明
required 必须包含该字段一次
optional 包含该字段零次或一次
repeated 该字段可以重复任意多次,生成的代码是一个列表

注意:=后面不是赋值,而是字段唯一标识符,主要是编码的时候使用,required、defalut在3.x已经废弃,尽量不要使用。

字段唯一标识符比较重要,它必须是唯一,就是不能重复,取值范围是:[1,536870911],也就是[1,2^29 - 1],其中19000到19999是protobuf保留使用。

这个值在[1,15]使用一个字节,[16,2047]使用2个字节。

3. 编译proto文件

protoc ./book.proto --java_out=./
protoc -IG:\zm --java_out=G:\tmp --java_out=G:\tmp\java G:\zm\book.proto
protoc -IG:\doc\java\protobuf --java_out=G:\tmp G:\doc\java\protobuf\book.proto
protoc -IG:\zm --java_out=G:\tmp G:\zm\user.proto G:\zm\book.proto

编译命令非常简单只需要指定输出目录和要编译的proto文件就可以,输出目录选择对应的语言,如果使用java,就使用--java_out,如果是c\c++就使用--cpp_out,其他同理。

还有一个参数比较重要,-I,或者 --proto_path,指定输入proto的目录,如果没有指定默认就是当前目录。

这个目录一定要和要编译的proto文件的目录一直,否则没有办法编译,就算指定了输入目录,也不能省略proto文件的路径。

4. 系列化与反系列化

我们还是使用下面的proto文件来编译看一下:

syntax = "proto2";

option java_package = "vip.mycollege.netty.proto";
option java_outer_classname = "BookProto";

//书籍
message Book {
	optional uint32 id = 1;//书籍id 
	optional string name = 2;//书籍名称
	repeated BookComment bookComments = 3;//书籍评论

}

//评论
message BookComment {
	optional uint64 id = 1;//评论id
	optional uint32 bookId = 2;//书籍id
	optional string content = 5;//评论内容
}

protoc编译生成的代码BookProto比较多,这里就不贴出来了,主要需要注意:

  1. 每一个proto文件生成的类都是外部类,具体的message是其中的内部类。
  2. 每个message都会有一个builder内,可以用builder类来创建对应类的实例
  3. 系列化为字节码的时候使用对应类的toByteArray方法
  4. 反系列化为类实例的时候使用对应类的parseFrom方法

看下面的简单例子:

@Test
public void testBookProto() throws InvalidProtocolBufferException {
	BookProto.BookComment bookComment = BookProto.BookComment.newBuilder()
			.setBookId(1)
			.setId(1)
			.setContent("你好啊").build();

	BookProto.Book book = BookProto.Book.newBuilder()
			.addBookComments(bookComment)
			.setId(1)
			.setName("飞龙在天").build();

	byte[] bytes = book.toByteArray();

	System.out.println(Arrays.toString(bytes));

	BookProto.Book parseBook = BookProto.Book.parseFrom(bytes);
	System.out.println("bookId=" + parseBook.getId() + " bookName=" + parseBook.getName());
	List<BookProto.BookComment> bookCommentsList = parseBook.getBookCommentsList();
	for(BookProto.BookComment comment : bookCommentsList){
		System.out.println("commentId=" + comment.getId() + " content=" + comment.getContent());
	}
}

5. 数据类型

protobuf数据类型 Java数据类型 说明
bool boolean boolean
bytes ByteString 任意字节,长度不能超过2^32
float float float
int32 int 使用变长编码,如果有负数使用sint32效率更高
int64 long 使用变长编码,如果有负数使用sint64效率更高
double double double
uint32 int 使用变长编码
uint64 long 使用变长编码
sint32 int 使用变长编码,编码负数的效率比int32更高
sint64 long 使用变长编码,编码负数的效率比int64更高
string String 7位ASCII,或者UTF-8编码,长度不能超过2^32
fixed32 int 4字节,当值大于2^28时,比uint32效率更高
fixed64 long 8字节,当值大于2^56时,比uint64效率更高
sfixed32 int 4字节
sfixed64 long 8字节

要知道为什么?看下面protobuf编码。

6. protocol buffer编码

首先,先看一下,写类型分类表,什么意思后面介绍。

wire_type wire类型 属于该分类的数据类型
0 Varint int32、int64、uint32、uint64、sint32、sint64、bool、enum
1 64-bit fixed64、sfixed64、double
2 Length-delimited string、bytes、embedded messages、packed repeated fields
3 Start group groups(废弃)
4 End group groups(废弃)
5 32-bit fixed32、sfixed32、float

6.1 可变长整型(varint)

简单的说对于Varint分类的数据类型,例如int32,写数据的时候是按1个字节分组写的,每个字节使用7位,最高位是msb,这个不是骂人的意思,而是指most significant bit,用来标识后面还有没有更多字节,如果是1表示还有更多字节,如果是0表示该数据没有更多字节,结束了。

对于单字节来说没有msb。

举个例子,protobuf编码之后的二进制流是:

1010 1100 0000 0010

这个就分成2个字节:

1010 1100
0000 0010

第一个字节高位是1,表示第二个字节也是这个值的字节数据。

第一个字节高位是0,表示后面没有该值的字节数据。

然后去掉高位:

010 1100
000 0010

低字节到高字节连接起来:

000 0010 010 1100

就是十进制的:300

6.2 消息结构

每个字段在message流中都又一个varint的key值和它绑定,这个key值就是通过我们前面说的field_number和wire_type计算的:

(field_number << 3) | wire_type

也就是这个key的最后3位是wire_type

我们使用一个int32值为150在proto编码之后的数据为例:

00001000 10010110 00000001

首先:

00001000

第一位是0,说明该数据后面没有字节了,所以去掉第一位msb数据就是:

000 1000

最后三位是wire_type,值是000,也就是0,查一下表,我们知道后面的数据类型是Varint。

把wire_type的3位去掉,剩下的是字段标识,就是我们定义message字段等号后面的值,这里是1。

然后,读一个字节:

10010110

第一位msb为1,说明这个接下来的字节也属于这个数据,所以再读一个字节:

00000001

这一次msb为0,说明这个数据后面没有字节了,所以数据就是:

10010110 00000001

去掉msb:

0010110
0000001

低位到高位连接:

000 0001 ++ 001 0110 = 10010110(BIN) = 150(DEC)

上面的编码,可以使用下面的例子验证:

syntax = "proto2";

option java_package = "vip.mycollege.netty.proto";
option java_outer_classname = "HelloProto";


message Hello {
	optional int32 id = 1;
}
 @Test
public void testHelloProto() {
	HelloProto.Hello hello = HelloProto.Hello.newBuilder()
			.setId(150)
			.build();

	byte[] bytes = hello.toByteArray();
	System.out.println("150 proto编码后十六进制:" + byte2HexStr(bytes));
	System.out.println("150 proto编码后二进制  :" + byte2BinStr(bytes));

	hello = HelloProto.Hello.newBuilder()
			.setId(300)
			.build();

	bytes = hello.toByteArray();
	System.out.println("300 proto编码后十六进制:" + byte2HexStr(bytes));
	System.out.println("300 proto编码后二进制  :" + byte2BinStr(bytes));

}


/**
	* 字节数组转二进制字符串
	* @param data
	* @return
	*/
public static String byte2BinStr(byte[] data) {
	StringBuffer result = new StringBuffer();
	for (int i = 0; i < data.length; i++) {
		String bin = Integer.toString(data[i] & 0xff, 2);
		int fillLeng = 8 - bin.length();
		while (fillLeng-- > 0){
			result.append("0");
		}
		result.append(bin).append(" ");
	}
	return result.toString().substring(0, result.length() - 1);
}

/**
	* 字节数组转换为十六进制的字符串
	**/
public static String byte2HexStr(byte[] data) {
	StringBuffer result = new StringBuffer();
	for (int i = 0; i < data.length; i++) {
		if ((data[i] & 0xff) < 0x10)
			result.append("0");
		result.append(Integer.toString(data[i] & 0xff, 16)).append(" ");
	}
	return result.toString().toUpperCase();
}

6.3 sint32 与 sint64

proto在编码sint32 与 sint64会映射为,正数:

sint32: (n << 1) ^ (n >> 31)

sint64: (n << 1) ^ (n >> 63)

当有负数的时候会比int32 和 int64效率更高

6.4 string

String的wire_type是2,例如:

Hello World!

proto编码之后的十六进制就是:

2A 0C 48 65 6C 6C 6F 20 57 6F 72 6C 64 21

proto编码之后的二进制就是:

00101010 00001100 01001000 01100101 01101100 01101100 01101111 00100000 01010111 01101111 01110010 01101100 01100100 00100001

首先还是每一个数据前面首先是一个varint类型的key:

00101010

第一位msb是0,说明key只有一个字节,最后3位是wire_type:

010

wire_type是2,说明是一个Length-delimited类型。

剩下的是field_number:

0101

说明这个字段的唯一标识是5

因为是Length-delimited,所以接下来的字节是数据长度

读一个字节:

00001100

第一位msb是0,说明字节长度只有一个字节,长度是0001100(BIN),也就是12(DEC)

所以,接下来的12个字节,都是这个字段的数据。

可以使用下面的代码来验证String编码:

syntax = "proto2";

option java_package = "vip.mycollege.netty.proto";
option java_outer_classname = "WorldProto";


message World {
	optional string name = 5;
}
@Test
public void testWorldProto() {
	WorldProto.World world = WorldProto.World.newBuilder()
			.setName("Hello World!")
			.build();

	byte[] bytes = world.toByteArray();
	System.out.println("proto编码后十六进制:" + byte2HexStr(bytes));
	System.out.println("proto编码后二进制  :" + byte2BinStr(bytes));

	StringBuilder sb = new StringBuilder();
	for(int i=0;i<300;i++){
		sb.append("i");
	}
	world = WorldProto.World.newBuilder()
			.setName(sb.toString())
			.build();

	bytes = world.toByteArray();
	System.out.println("proto编码后十六进制:" + byte2HexStr(bytes));
	System.out.println("proto编码后二进制  :" + byte2BinStr(bytes));
}

7. protocol buffer2.x 与 protocol buffer3.x

相对于2.x,3.x的重大变化有:

  1. required被彻底废弃
  2. optional名称变为了singular
  3. repeated默认使用packed
  4. 废弃了default
  5. 枚举类型的第一个字段必须为0
  6. 彻底废弃了分组groups
  7. 废弃了扩展Extensions
  8. 新增了Any类型代替扩展
  9. 新增JSON映射
  10. 新增语言Go、Ruby、JavaNano

知道了2.x与3.x有哪些不同,我们就可以避开一些坑。

例如,在2.x中定义数字标量的时候,就可以使用packed来提高效率:

repeated int32 age = 1 [packed=true];

不实用默认值default,而是让protobuf提供:

数据类型 默认值
bool false
enum 第一个枚举值为0
bytes 空bytes
string 空字符串
numeric 0
repeated 空list

不使用required。

8. 资料文档

protoc新版本下载

protoc2.5版本下载

protocol buffer github

protocol buffer javatutorial

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部