概述
文章目录
- 1. 简介
- 2. 定义message
- 3. 编译proto文件
- 4. 系列化与反系列化
- 5. 数据类型
- 6. protocol buffer编码
- 6.1 可变长整型(varint)
- 6.2 消息结构
- 6.3 sint32 与 sint64
- 6.4 string
- 7. protocol buffer2.x 与 protocol buffer3.x
- 8. 资料文档
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:tmpjava G:zmbook.proto
protoc -IG:docjavaprotobuf --java_out=G:tmp G:docjavaprotobufbook.proto
protoc -IG:zm --java_out=G:tmp G:zmuser.proto G:zmbook.proto
编译命令非常简单只需要指定输出目录和要编译的proto文件就可以,输出目录选择对应的语言,如果使用java,就使用–java_out,如果是cc++就使用–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比较多,这里就不贴出来了,主要需要注意:
- 每一个proto文件生成的类都是外部类,具体的message是其中的内部类。
- 每个message都会有一个builder内,可以用builder类来创建对应类的实例
- 系列化为字节码的时候使用对应类的toByteArray方法
- 反系列化为类实例的时候使用对应类的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的重大变化有:
- required被彻底废弃
- optional名称变为了singular
- repeated默认使用packed
- 废弃了default
- 枚举类型的第一个字段必须为0
- 彻底废弃了分组groups
- 废弃了扩展Extensions
- 新增了Any类型代替扩展
- 新增JSON映射
- 新增语言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. 资料文档
protobuf示例代码文档编译器下载
protoc新版本下载
protoc2.5版本下载
protocol buffer github
protocol buffer javatutorial
最后
以上就是害怕裙子为你收集整理的protobuf示例与编码详解的全部内容,希望文章能够帮你解决protobuf示例与编码详解所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复