关于Protobuf的学习
2018-01-11 14:14
363 查看
简介
protoBuf是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。优点
与同类型的数据交换格式相比(诸如json,xml),由于protobuf是基于二进制数据传输格式,因此它具有高效的解析速度和更小的体积,并且由于它是直接基于.proto文件生成对应语言的数据结构,因此它的转换过程更加简单直接。同时因为protobuf实现都包含了相应语言的编译器以及库文件,它将比传统的基于一套规范解析更加精准可控,误解的概率更低。综上优点 :
高效的解析速度
小巧的传输体积
直接上手,简单易用
解析可控,误解率低
缺点
与传统的数据交换格式相比,由于基于二进制数据传输格式,protobuf可读性为零。同时虽然protobuf强调的是跨平台性,但相较于json,xml来说,protobuf的语言覆盖率偏低,并且手动开发一个自定义protobuf的工作量也偏大。综上缺点 :
可读性低
语言支持相对少
用途
作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于分布式应用之间的数据通信或者异构环境1,并且在传统的cs架构的环境中,protobuf也可以用作客户端服务端公用的数据交换格式。简单上手
下面介绍在Windows下使用protobuf进行python与java互通的helloword程序下载protobuf
由于国内google被墙,只能在github上下载下载地址在windows下载protoc-XXX-win32.zip解压即可
配置一个.proto
//文件名test.proto //定义包名 package test; //protobuf的编译器版本 syntax = "proto3"; //java的包名 option java_package = "test.protobuf"; //java中主类的名称(及public修饰的类的名称) option java_outer_classname = "MProtobuf"; message request{ //1为msg在request中的field_num,在后面会讲 string msg = 1; string commet = 2; }
生成对应的Java和Python文件
将刚才写好的test.proto移到解压后文件的bin目录并在该目录下打开命令窗口。
输入:
protoc.exe --java_out=./ test.proto
在当前目录生成java文件
输入:
protoc.exe --python_out=./ test.proto
在当前目录生成python文件
在当前目录下test文件就是我们生成的java文件,点进去可见到MProtobuf.java
这就是我们在:
option java_outer_classname = "MProtobuf";
定义的outer_classname
分析生成文件结构
MProtobuf由requestOrBuilder和request构成我们可以通过request中的newBuilder()构建一个requestOrBuilder
并通过requestOrBuilder构建一个request
在request中有writeTo方法:
public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { if (!getMsgBytes().isEmpty()) { com.google.protobuf.GeneratedMessageV3.writeString(output, 1, msg_); } if (id_ != 0) { output.writeInt32(2, id_); } unknownFields.writeTo(output); }
能方便将request写进流里
通过request中的parseFrom方法:
public static test.protobuf.HelloWorldProto.HelloWorld parseFrom( com.google.protobuf.CodedInputStream input) throws java.io.IOException { return com.google.protobuf.GeneratedMessageV3 .parseWithIOException(PARSER, input); }
能方便将流转换成request对象
编写Server.java
首先生成的MProtobuf.java是依赖于com.google.protobuf的,所有先下载它的jar包编写Server.java:
import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import com.google.protobuf.CodedInputStream; import test.protobuf.MProtobuf.request; public class Server { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(12345); for(;;){ Socket socket = server.accept(); InputStream ism = socket.getInputStream(); CodedInputStream csm = CodedInputStream.newInstance(ism); request req = request.parseFrom(csm); System.out.println(req); } } }
编写client.py
同样在python中依赖protobufpip install protobuf
在生成的test_pb2.py的文件下建立client.py
编写client.py:
import test_pb2 as tp import socket socket = socket.socket() socket.connect(("127.0.0.1",12345)) req = tp.request() req.msg = "helloworld" req.commet = "power by protobuf" socket.sendall(req.SerializeToString())
运行
先运行Java,在运行client.py控制台输出:
msg: "helloworld" commet: "power by protobuf"
运行成功
深入
字段修饰符2
required对于required的字段而言,字段初值是必须要提供的,否则字段的便是未初始化的
对于修饰符为required的字段,序列化的时候必须给予初始化,否则程序运行会异常
optional
对于optional的字段而言,如果未进行初始化,那么一个默认值将赋予该字段编号
也可以指定默认值,如下示例所示.
optional string name = 1[default = "ssochi"]
repeated
对于repeated的字段而言,该字段可以重复多个,即每个编码单元可能有多个该字段
在高级语言里面,我们可以通过数组来实现,而在proto定义文件中可以使用repeated来修饰,从而达到相同目的。当然,出现0次也是包含在内的。
字段类型
.proto Type | Java Type | notes |
---|---|---|
int32 | int | 使用变长编码,在值为负数的情况下效率低,应采用sin32代替 |
int64 | long | 使用变长编码,在值为负数的情况下效率低,应采用sin64代替 |
uint32 | int | 使用变长编码 |
uint64 | long | 使用变长编码 |
sint32 | int | 使用变长编码,有符号整型,在值为负数的情况下效率高 |
sint64 | long | 使用变长编码,有符号整型,在值为负数的情况下效率高 |
fixed32 | int | 固定四个字节.如果值经常大于2^28使用fixed32会比使用uint32更高效 |
fixed64 | long | 固定八个字节.如果值经常大于2^56使用fixed64会比使用uint64更高效 |
sfixed32 | int | 有符号,固定四个字节 |
sfixed64 | long | 有符号,固定八个字节 |
bool | boolean | |
double | double | |
string | String | string必须一致包含UTF-8编码或者7-bit ASCII字符串 |
bytes | ByteString | 可以包含任意字节序列 |
字段类型对应二进制类型
字段类型 | 二进制类型 | 二进制编码值 |
---|---|---|
int32,int64,uint32,uint64,sint32,sint64,bool,enum | Varint(可变长度int) | 0 |
fixed64,sfixed64,double | double | 1 |
string,bytes,inner messages(内部嵌套),packaed repeated fields(repeated字段) | Length-delimited | 2 |
groups(deprecated) | Start group | 3 |
groups(deprecated) | Endd group | 4 |
fixed32,sfixed32,float | 32bit固定长度 | 5 |
field_num
每个消息的字段都有一个唯一的数字标签,这些标签用来表示你的字段在二进制消息(message binary format)中处的位置。并且一旦指定标签号,在使用过程中是不可以更改的,标记这些标签号在1-15的范围内每个字段需要使用1个字节用来编码这一个字节包括字段所在的位置和字段的类型。标签号在16-2047需要使用2个字节来编码。所以你最好将1-15的标签号为频繁使用到的字段所保留。如果将来可能会添加一些频繁使用到的元素,记得留下一些1-15标签号。最小可指定的标签号为1,最大的标签号为2^29 - 1或者536870911。不能使用19000-19999的标签号这些标签号是为protobuf内部实现所保留的,如果你在.proto文件内使用了这些标签号Protobuf编译器将会报错!
结构
消息
protobuf中使用message定义一个消息,在同一个包内能直接引用,在不同包内则需要先importmessage request{ string msg = 1; string commet = 2; }
枚举
enum PhoneType //枚举消息类型 { MOBILE = 0; //proto3版本中,首成员必须为0,成员不应有相同的值 HOME = 1; WORK = 2; }
Map
protobuf v2是不支持Map数据结构的,官方给出的不就方法是通过如下代码代替Map:message MapFieldEntry { key_type key = 1; value_type value = 2; } repeated MapFieldEntry map_field = N;
官方给出的解释是:The map syntax is equivalent to the following on the wire, so protocol buffers implementations that do not support maps can still handle your data
在protobuf v3 的较新版本已经支持Map3,可以通过如下代码申明Map
map<string, string> values= 1;
嵌套
message定义中可以嵌套定义message,enum嵌套定义的单元外部可见,引用路径由外到内逐层引用
扩展字段
protobuf通过extension解决数据结构之间的不能派生的问题,以此来达到减少重复工作量和便于维护代码的目的。message BaseDataType{ extensions 100 to max; //标识此字段可扩展,此处可指定扩展字段的ID有效范围,to max表示字段范围至最大值 optional string field1 = 1; optional string field2 = 2; }
message ExtendDataType{ extend BaseDataType{ optional ExtendDataType extendData = 100; optional int32 extendValue = 101; } optional string extendField1 = 1; optional string extendField2= 2; }
编码
Varint编码规则
Varints是将一个整数序列化为一个或多个Bytes的方法,越小的整数,使用的Bytes越少每个byte最高位(msb)是标志位,0表示是最后一个byte,1表示该字段值还有后续byte
每个byte低7位存放数值
Varints使用Little Endian(小端)字节序
编码试例:
举例: 300 的二进制为 10 0101100 第一位:1(有后续) + 0101100 第二位:0(无后续) + 0000010 最终结果: 101011000000010
可见,由于是小端字节序,值越小Varints序列化后使用的Bytes越少。
然而负数在补码的首位是1,则使用Varint序列化后,所有负数都相当于非常大的数,
这造成了负数序列化后使用的bytes增加,为了解决这个问题对于负数采用sint32或sint64。
而sint先采用Zigzag方法避免上述问题再通过Varint序列化。
Zigzag编码规则
对于sint32采用(n<<1)^(n>>31)对于sint32采用(n<<1)^(n>>63)
其中>>操作当操作数为负数时高位补1正数时高位补1
Zigzag编码将整数重新映射到定义域,使得整数映射后的补码长度和绝对值的大小成正相关:
原始值 | 编码后的值 | 编码后的补码 |
---|---|---|
0 | 0 | 00000000 |
-1 | 1 | 00000001 |
1 | 2 | 00000010 |
-2 | 3 | 00000011 |
3 | 4 | 00000100 |
… | … | … |
length-delimited编码
string,bytes都属于length-delimited编码,length-delimited(wire_type=2)的编码方式:key+length+contentkey的编码方式是统一的
length采用varints编码方式
content就是由length指定的长度的Bytes
消息编码规则
message都是以一组或多组key-value对组成,key和value分别采用不同的编码方式序列化时,将message中所有key-value序列化成二进制字节流。反序列化时,解析出所有key-value对,
如果遇到无法识别的类型,则直接跳过。这种机制保证了旧有的编/解码在协议添加新的字段时,依旧可以正常工作
key由两部分组成,一部分是在定义消息时对字段的编号(field_num4),另一部分是字段类型(wire_type,编号最大不超过536870911.
key编码方式filed_num<<3|wire_type,编码后的二进制长度是变长的
value编码则根据字段类型进行编码.
生成代码分析
接下来我将通过protoc.exe生成的java文件,分析protobuf的编解码过程首先我们先创建一个包含protobuf所有类型的.proto
//文件名protobufStruct.proto //protobuf 版本 syntax = "proto3"; //java的包名 option java_package = "struct.protobuf"; //java中主类的名称(及public修饰的类的名称) option java_outer_classname = "structs"; //基本类型 message baseStruct{ int32 int32value = 1; int64 int64value = 2; uint32 uint32value = 3; uint64 uint64value = 4; sint32 sint32value = 5; sint64 sint64value = 6; fixed32 fixed32value = 7; fixed64 fixed64value = 8; sfixed32 sfixed32value = 9; sfixed64 sfixed64value = 10; bool boolvalue = 11; double doublevalue = 12; string stringvalue = 13; bytes bytesvalue = 14; //枚举 enum PhoneType{ MOBILE = 0; HOME = 1; WORK = 2; } } //复合类型 message complexStruct{ //map map<int32, string> mapvalues= 1; //list repeated MapFieldEntry map_field = 2; } message MapFieldEntry { int32 key = 1; string value = 2; }
通过执行cmd指令生成java文件
protoc.exe --java_out=./ protobufStruct.proto
我们可以看到baseStructOrBuilder:
public interface baseStructOrBuilder extends // @@protoc_insertion_point(interface_extends:baseStruct) com.google.protobuf.MessageOrBuilder { /** * <code>int32 int32value = 1;</code> */ int getInt32Value(); /** * <code>int64 int64value = 2;</code> */ long getInt64Value(); /** * <code>uint32 uint32value = 3;</code> */ int getUint32Value(); /** * <code>uint64 uint64value = 4;</code> */ long getUint64Value(); /** * <code>sint32 sint32value = 5;</code> */ int getSint32Value(); /** * <code>sint64 sint64value = 6;</code> */ long getSint64Value(); /** * <code>fixed32 fixed32value = 7;</code> */ int getFixed32Value(); /** * <code>fixed64 fixed64value = 8;</code> */ long getFixed64Value(); /** * <code>sfixed32 sfixed32value = 9;</code> */ int getSfixed32Value(); /** * <code>sfixed64 sfixed64value = 10;</code> */ long getSfixed64Value(); /** * <code>bool boolvalue = 11;</code> */ boolean getBoolvalue(); /** * <code>double doublevalue = 12;</code> */ double getDoublevalue(); /** * <code>string stringvalue = 13;</code> */ java.lang.String getStringvalue(); /** * <code>string stringvalue = 13;</code> */ com.google.protobuf.ByteString getStringvalueBytes(); /** * <code>bytes bytesvalue = 14;</code> */ com.google.protobuf.ByteString getBytesvalue(); /** * <code>.baseStruct.PhoneType type = 15;</code> */ int getTypeValue(); /** * <code>.baseStruct.PhoneType type = 15;</code> */ struct.protobuf.structs.baseStruct.PhoneType getType(); }
这里java类型与protobuf的类型关系完全复合上面的字段类型表
将message序列化使用writeTo方法:
public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { if (int32Value_ != 0) { output.writeInt32(1, int32Value_); } if (int64Value_ != 0L) { output.writeInt64(2, int64Value_); } if (uint32Value_ != 0) { output.writeUInt32(3, uint32Value_); } if (uint64Value_ != 0L) { output.writeUInt64(4, uint64Value_); } if (sint32Value_ != 0) { output.writeSInt32(5, sint32Value_); } if (sint64Value_ != 0L) { output.writeSInt64(6, sint64Value_); } if (fixed32Value_ != 0) { output.writeFixed32(7, fixed32Value_); } if (fixed64Value_ != 0L) { output.writeFixed64(8, fixed64Value_); } if (sfixed32Value_ != 0) { output.writeSFixed32(9, sfixed32Value_); } if (sfixed64Value_ != 0L) { output.writeSFixed64(10, sfixed64Value_); } if (boolvalue_ != false) { output.writeBool(11, boolvalue_); } if (doublevalue_ != 0D) { output.writeDouble(12, doublevalue_); } if (!getStringvalueBytes().isEmpty()) { com.google.protobuf.GeneratedMessageV3.writeString(output, 13, stringvalue_); } if (!bytesvalue_.isEmpty()) { output.writeBytes(14, bytesvalue_); } if (type_ != struct.protobuf.structs.baseStruct.PhoneType.MOBILE.getNumber()) { output.writeEnum(15, type_); } unknownFields.writeTo(output); }
可以看出,当一个值为空,直接跳过这个值。如果这个值存在,则写入它的filed_num和它编码后的值,
前面讲过对于不同类型的值,有不同的编码方式,比如int32使用先Zigzag编码再Varint编码,
而string通过length-delimited编码,等。
再看看complexStruct的builder和WriteTo方法
public interface complexStructOrBuilder extends // @@protoc_insertion_point(interface_extends:complexStruct) com.google.protobuf.MessageOrBuilder { /** * <code>map<int32, string> mapvalues = 1;</code> */ int getMapvaluesCount(); /** * <code>map<int32, string> mapvalues = 1;</code> */ boolean containsMapvalues( int key); /** * Use {@link #getMapvaluesMap()} instead. */ @java.lang.Deprecated java.util.Map<java.lang.Integer, java.lang.String> getMapvalues(); /** * <code>map<int32, string> mapvalues = 1;</code> */ java.util.Map<java.lang.Integer, java.lang.String> getMapvaluesMap(); /** * <code>map<int32, string> mapvalues = 1;</code> */ java.lang.String getMapvaluesOrDefault( int key, java.lang.String defaultValue); /** * <code>map<int32, string> mapvalues = 1;</code> */ java.lang.String getMapvaluesOrThrow( int key); /** * <code>repeated .MapFieldEntry map_field = 2;</code> */ java.util.List<struct.protobuf.structs.MapFieldEntry> getMapFieldList(); /** * <code>repeated .MapFieldEntry map_field = 2;</code> */ struct.protobuf.structs.MapFieldEntry getMapField(int index); /** * <code>repeated .MapFieldEntry map_field = 2;</code> */ int getMapFieldCount(); /** * <code>repeated .MapFieldEntry map_field = 2;</code> */ java.util.List<? extends struct.protobuf.structs.MapFieldEntryOrBuilder> getMapFieldOrBuilderList(); /** * <code>repeated .MapFieldEntry map_field = 2;</code> */ struct.protobuf.structs.MapFieldEntryOrBuilder getMapFieldOrBuilder( int index); }
可以看出protobuf中的repected对应java.util.list
map对应java.util.Map
writeTo方法:
public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { com.google.protobuf.GeneratedMessageV3 .serializeIntegerMapTo( output, internalGetMapvalues(), MapvaluesDefaultEntryHolder.defaultEntry, 1); for (int i = 0; i < mapField_.size(); i++) { output.writeMessage(2, mapField_.get(i)); } unknownFields.writeTo(output); }
可以看出,被repected修饰的字段,这个字段中每个子项都用相同的field_num。
这样的优点是不用特殊标识就能标识一个集合字段。
但它的缺点很明显,整个protobuf只能标识一种集合类型。
也就是说map肯定转成list然后再编码的
protected static <V> void serializeIntegerMapTo( CodedOutputStream out, MapField<Integer, V> field, MapEntry<Integer, V> defaultEntry, int fieldNumber) throws IOException { Map<Integer, V> m = field.getMap(); if (!out.isSerializationDeterministic()) { serializeMapTo(out, m, defaultEntry, fieldNumber); return; } // Sorting the unboxed keys and then look up the values during serialziation is 2x faster // than sorting map entries with a custom comparator directly. int[] keys = new int[m.size()]; int index = 0; for (int k : m.keySet()) { keys[index++] = k; } Arrays.sort(keys); for (int key : keys) { out.writeMessage(fieldNumber, defaultEntry.newBuilderForType() .setKey(key) .setValue(m.get(key)) .build()); } } /** Serialize the map using the iteration order. */ private static <K, V> void serializeMapTo( CodedOutputStream out, Map<K, V> m, MapEntry<K, V> defaultEntry, int fieldNumber) throws IOException { for (Map.Entry<K, V> entry : m.entrySet()) { out.writeMessage(fieldNumber, defaultEntry.newBuilderForType() .setKey(entry.getKey()) .setValue(entry.getValue()) .build()); }
通过serializeIntegerMapTo和serializeMapTo方法可知:
当Map最终是转换成list来编码的,其中map转换为list有两种顺序,
一种是按照map的迭代顺序存入list
一种是按照key的字典序
综上,我们通过分析代码理解了protobuf的编码过程,当然解码过程其实也就是把编码过程倒过来。
解码需要注意的是zigzag解码方式:
当使用支持无符号移位的语言时可以使用:
n = (n >>> 1)^(-(n & 1))
当使用像python这类不支持无符号移位的语言可以使用:
n = (n ^ (-(n & 1)) )>> 1
异构网络环境(Heterogeneous Network Environments)是指由不同制造商生产的计算机和系统组成的网络环境。这些计算机系统运行不同的操作系统和通信协议,想统一其计算机资源的机构通常会面临集成异种机系统的任务。一般地,每一个部门或分部已经根据操作系统、局域网拓扑结构、通信协议、应用程序、电子函件系统以及其它因素规定了自己的网络需要。企业网的目标就是使这些分散的资源可以进行互连和互操作,以便网络用户可以和其他用户一起共享文件和电子函件,或者访问企业的数据资源。 ↩
为了更好的跨语言性,google已将protobuf v2中的required删去,默认都是optional。并且也不在使用默认值及[default = * ],更多详见https://github.com/google/protobuf/blob/master/CHANGES.txt。 ↩
在知乎上看见一条关于protobuf v3中使用Map的警告:protobuf v3用了Emitter,Unity3d的il2cpp是不支持的,用Unity的留心了。 ↩
field_num及为
string name = 1其中1就是name的field_num。 ↩
相关文章推荐
- 关于protobuf的学习
- 【protobuf】关于数字alloc边界的小技巧
- protobuf-c的学习总结
- Ubuntu下关于安装opencv3.x时一直downloading protobuf-cpp-3.1的问题
- golang Protobuf学习
- 深度学习Caffe实战笔记(12)Caffe 使用Python接口No module named google.protobuf.internal修复方案
- 关于protobuf javascript的版本
- google protobuf 反射机制学习笔记
- GOOGLE PROTOBUF学习与使用心得
- protobuf-net 学习手记
- 照着例子学习 protobuf-lua
- 关于protobuf中的field_number范围的解释
- 关于google protobuf编译找不到不到google\protobuf\stubs\common.h头文件的问题
- Protobuf学习 - 入门
- protobuf 学习 收藏的文章
- google protobuf学习笔记一:windows下环境配置
- 关于json与protobuf的材料
- 关于protobuf的问题
- netty学习六:第一个protobuf小demo
- Google protobuf 学习