简介
protocol buffers是Google的一个灵活的、高效的、自动化的用于对结构化数据进行序列化的协议,与json、xml相比,protocol buffers序列化后的码流更小、速度更快、操作更简单。你只需要将要被序列化的数据结构定义一次(使用.proto文件定义),便可以使用特别生成的源代码(使用protobuf提供的生成工具protoc)轻松的使用不同的数据流完成对这些结构数据的读写操作,即使你使用不同的语言(protobuf的跨语言支持特性)。你甚至可以更新你的数据结构的定义(就是更新.proto文件内容)而不会破坏依“老”格式编译出来的程序。
使用简介
1、使用PB之前需要先下载个协议编译器,项目地址
https://github.com/protocolbuffers/protobuf.git
你可以选择自己下载源码编译,编译后可以获取到一个protoc.exe的执行文件,当然嫌麻烦可以直接使用已经编译好的执行文件
https://github.com/protocolbuffers/protobuf/releases/download/v3.9.0/protoc-3.9.0-win64.zip
2、PB协议的描述文件以一个 .proto 后缀的文件,我们可以先创建个简单的PB文件
如Interfaces.proto, 内容如下
syntax = "proto3";
message Player{
int64 id=1;
string name=2;
int32 age=3;
repeated int32 skillIds=4;
}
message可以理解为class,或者包名
3、类型对比
PB | JAVA |
---|---|
bool | boolean |
int32 | int |
int64 | long |
double | double |
float | float |
string | String |
map | Map |
repeat int32 | List |
4.编译生成目标语言文件
C++
protoc Interfaces.proto --cpp_out=./src
Java
protoc Interfaces.proto --java_out=./src
Python
protoc Interfaces.proto --python_out=./src
C#
protoc Interfaces.proto --csharp_out=./src
序列化对比
下面来对比下PB、 java序列化、json的序列化结果对比
PB
/**
* @Author hanjie.l
*/
public class PB2Bytes {
public static void main(String agrs[]) throws InvalidProtocolBufferException {
byte[] tobytes = tobytes();
System.out.println("序列化结果:" + Arrays.toString(tobytes));
System.out.println("长度:" + tobytes.length);
System.out.println("==================================");
Interfaces.Player player = toPlayer(tobytes);
System.out.println("反序列化结果:");
System.out.println("id:" + player.getId());
System.out.println("name:" + player.getName());
System.out.println("age:" + player.getAge());
System.out.println("skills:" + Arrays.toString(player.getSkillIdsList().toArray()));
}
/**
* 序列化
*/
public static byte[] tobytes(){
//获取构造器
Interfaces.Player.Builder newBuilder = Interfaces.Player.newBuilder();
//设置参数
newBuilder.setId(1)
.setName("peter")
.setAge(18)
.addSkillIds(1001)
.addSkillIds(1002);
//构造出player
Interfaces.Player player = newBuilder.build();
//序列化成字节
byte[] byteArray = player.toByteArray();
return byteArray;
}
/**
* 反序列化
* @param byteArray
*/
public static Interfaces.Player toPlayer(byte[] byteArray) throws InvalidProtocolBufferException {
//解析成player
Interfaces.Player player = Interfaces.Player.parseFrom(byteArray);
return player;
}
}
序列化结果:[8, 1, 18, 5, 112, 101, 116, 101, 114, 24, 18, 34, 4, -23, 7, -22, 7]
长度:17
java序列化
/**
* @Author hanjie.l
*/
public class Java2Bytes {
public static void main(String[] args) throws Exception {
byte[] tobytes = tobytes();
System.out.println("序列化结果:" + Arrays.toString(tobytes));
System.out.println("长度:" + tobytes.length);
System.out.println("================================");
Player player = toPlayer(tobytes);
System.out.println("反序列化结果:");
System.out.println("id:" + player.getId());
System.out.println("name:" + player.getName());
System.out.println("age:" + player.getAge());
System.out.println("skills:" + Arrays.toString(player.getSkillIds().toArray()));
}
/**
* 序列化
*
* @throws IOException
*/
public static byte[] tobytes() throws IOException {
// 玩家
Player player = new Player();
player.setId(1);
player.setName("peter");
player.setAge(18);
player.getSkillIds().add(1001);
player.getSkillIds().add(1002);
// 创建对象流
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(arrayOutputStream);
// 写入对象
objectOutputStream.writeObject(player);
// 获取字节数组
byte[] byteArray = arrayOutputStream.toByteArray();
return byteArray;
}
/**
* 反序列化
*
* @param byteArray
* @throws IOException
* @throws ClassNotFoundException
*/
public static Player toPlayer(byte[] byteArray) throws Exception {
// 创建输入流
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(byteArray));
Player player = (Player) inputStream.readObject();
return player;
}
}
序列化结果:[-84, -19, 0, 5, 115, 114, 0, 25, 99, 111, 109, 46, 115, 104, 97, 114, 101, 46, 112, 98, 46, 109, 111, 100, 101, 108, 46, 80, 108, 97, 121, 101, 114, 55, 91, 120, -117, 98, -118, 98, -90, 2, 0, 4, 73, 0, 3, 97, 103, 101, 74, 0, 2, 105, 100, 76, 0, 4, 110, 97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 8, 115, 107, 105, 108, 108, 73, 100, 115, 116, 0, 16, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 105, 115, 116, 59, 120, 112, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 1, 116, 0, 5, 112, 101, 116, 101, 114, 115, 114, 0, 19, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 65, 114, 114, 97, 121, 76, 105, 115, 116, 120, -127, -46, 29, -103, -57, 97, -99, 3, 0, 1, 73, 0, 4, 115, 105, 122, 101, 120, 112, 0, 0, 0, 2, 119, 4, 0, 0, 0, 2, 115, 114, 0, 17, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 73, 110, 116, 101, 103, 101, 114, 18, -30, -96, -92, -9, -127, -121, 56, 2, 0, 1, 73, 0, 5, 118, 97, 108, 117, 101, 120, 114, 0, 16, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 78, 117, 109, 98, 101, 114, -122, -84, -107, 29, 11, -108, -32, -117, 2, 0, 0, 120, 112, 0, 0, 3, -23, 115, 113, 0, 126, 0, 7, 0, 0, 3, -22, 120]
长度:276
json
/**
* @Author hanjie.l
*/
public class Json2Bytes {
public static void main(String[] args) {
Player player = new Player();
player.setId(1);
player.setName("peter");
player.setAge(18);
player.getSkillIds().add(1001);
player.getSkillIds().add(1002);
byte[] bytes = JSON.toJSONString(player).getBytes();
System.out.println("序列化结果:" + Arrays.toString(bytes));
System.out.println("长度:" + bytes.length);
}
}
序列化结果:[123, 34, 97, 103, 101, 34, 58, 49, 56, 44, 34, 105, 100, 34, 58, 49, 44, 34, 110, 97, 109, 101, 34, 58, 34, 112, 101, 116, 101, 114, 34, 44, 34, 115, 107, 105, 108, 108, 73, 100, 115, 34, 58, 91, 49, 48, 48, 49, 44, 49, 48, 48, 50, 93, 125]
长度:55
可以很明显发现PB的序列化结果要明显优于后两者
PB原理分析
总结了下PB优秀的序列化结果主要归功于以下3大特点
1、将部分对象信息以.proto文件的方式共享于 序列化方和反序列化方,节省了序列化中需要传递的信息
2、key-value结构
3、数字字节伸缩协议
在讲这个之前先了解下,正常序列化的时候是如何序列化的,以一个整形变量 int value=100来举例(小端)
int value = 100;
byte[] memory = new byte[4];
memory[0] = (byte) value;
memory[1] = (byte) (value >>> 8);
memory[2] = (byte) (value >>> 16);
memory[3] = (byte) (value >>> 24);
System.out.println(Arrays.toString(memory));// 输出结果: [100, 0, 0, 0]
下面再看下PB的序列化方式
int value = 100;
ByteBuffer pbs = ByteBuffer.allocate(5);
while (true) {
if ((value & ~0x7F) == 0) {
pbs.put((byte) value);
break;
} else {
pbs.put((byte) ((value & 0x7F) | 0x80));
value >>>= 7;
}
}
pbs.flip();
byte[] bytes = new byte[pbs.remaining()];
pbs.get(bytes);
System.out.println(Arrays.toString(bytes));// 输出结果: [100]
PB为了节省空间,用一个字节的第八位来表示是否还有下个字节,所以如果数字比较小,仅需占用1个字节, 当然因为占用了第八位,所以当数字比较大, 比如Integer.MAX_VALUE的时候需要占用额外多出一个字节,所以int类型在PB中占用的字节数是1-5个字节,以此类比long是占1-9个字节,这有点像弹簧,可能拉长也可能压短,所以我称它为数字伸缩协议
说完优点说缺点
通过实际使用会发现PB用起来很麻烦,需要写proto文件,再生成代码,最蛋疼的是代码有将近1k行, 有点蛋疼,我特么只是个简单的javaBean~~~~~
推荐个项目
https://github.com/jhunters/jprotobuf
这个项目引入以后就舒服多了, 你只需要像原来一样专心写自己的javaBean, 用java代码去生成.proto文件
/**
* @Author hanjie.l
*/
public class JPBPlayer {
@Protobuf(order = 1, fieldType = FieldType.INT64, description = "玩家id")
private long id;
@Protobuf(order = 2, fieldType = FieldType.STRING, description = "玩家名")
private String name;
@Protobuf(order = 3, fieldType = FieldType.INT32, description = "年龄")
private int age;
@Protobuf(order = 4, fieldType = FieldType.INT32, description = "技能列表")
private List skillIds=new ArrayList<>();
编解码
/**
* @Author hanjie.l
*/
public class JPB2Bytes {
public static void main(String[] args) throws IOException {
JPBPlayer player = new JPBPlayer();
player.setId(100);
player.setName("peter");
player.setAge(18);
player.getSkillIds().add(1001);
player.getSkillIds().add(1002);
Codec jpbPlayerCodec = ProtobufProxy.create(JPBPlayer.class);
byte[] bytes = jpbPlayerCodec.encode(player);
System.out.println("序列化结果:" + Arrays.toString(bytes));
System.out.println("长度:" + bytes.length);
long begin = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
jpbPlayerCodec.encode(player);
}
System.out.println("耗时:" + (System.currentTimeMillis() - begin));
System.out.println("==================================");
JPBPlayer jpbPlayer = jpbPlayerCodec.decode(bytes);
System.out.println("反序列化结果:");
System.out.println("id:" + jpbPlayer.getId());
System.out.println("name:" + jpbPlayer.getName());
System.out.println("age:" + jpbPlayer.getAge());
System.out.println("skills:" + Arrays.toString(jpbPlayer.getSkillIds().toArray()));
}
}
生产proto信息
System.out.println(ProtobufIDLGenerator.getIDL(JPBPlayer.class));
message JPBPlayer {
// 玩家id
int64 id=1;
// 玩家名
string name=2;
// 年龄
int32 age=3;
// 技能列表
repeated int32 skillIds=4;
}