我在上篇博文中简单介绍了一下protobuf,由于protobuf是Netty默认的序列化协议,再说它的性能也很高,接下来就详细的说一下protobuf协议!
Netty为什么要提供编解码框架
作为一个高性能的异步、NIO通信框架,编解码框架是Netty的重要组成部分。
Netty逻辑架构图
从网络读取的inbound消息,需要经过解码,将二进制的数据报转换成应用层协议消息或者业务消息,才能够被上层的应用逻辑识别和处理;同理,用户发送到网络的outbound业务消息,需要经过编码转换成二进制字节数组(对于Netty就是ByteBuf)才能够发送到网络对端。
编码和解码功能是NIO框架的有机组成部分,无论是由业务定制扩展实现,还是NIO框架内置编解码能力,该功能是必不可少的。
为了降低用户的开发难度,Netty对常用的功能和API做了装饰,以屏蔽底层的实现细节。编解码功能的定制,对于熟悉Netty底层实现的开发者而言,直接基于ChannelHandler扩展开发,
Netty针对编解码功能,它既提供了通用的编解码框架供用户扩展,又提供了常用的编解码类库供用户直接使用。在保证定制扩展性的基础之上,尽量降低用户的开发工作量和开发门槛,提升开发效率。
Netty预置的编解码功能列表如下:base64、Protobuf、JBoss Marshalling等。
下图来自Netty官网:GitHub地址—>codec源码
注:Netty默认的编解码协议是protobuf
protobuf 数据类型 | 描述 | Java 类型 |
---|---|---|
bool | 布尔类型 | boolean |
double | 64位浮点数 | double |
float | 32为浮点数 | float |
int32 | 32位整数 | int |
int64 | 无符号32位整数 | long |
uint32 | 64位整数 | int |
uint64 | 64为无符号整 | long |
sint32 | 32位整数,处理负数效率更高 | int |
sint64 | 64位整数 处理负数效率更高 | long |
fixed32 | 32位无符号整数 | int |
fixed64 | 64位无符号整数 | long |
sfixed32 | 32位整数、能以更高的效率处理负数 | int |
sfixed64 | 64为整数 | long |
string | 只能处理 ASCII字符 | String |
bytes | 用于处理多字节的语言字符、如中文 | ByteString |
enum | 可以包含一个用户自定义的枚举类型uint32 | enum |
message | 可以包含一个用户自定义的消息类型 | object of class |
1.几种限定符的含义
required: 必须赋值,不能为空,否则build一个“uninitialized” message会抛出一个RuntimeException异常,
optional:字段可以赋值,也可以不赋值。假如没有赋值的话,会被赋上默认值。
repeated: 该字段可以重复任意次数(包括0次),重复数据的顺序将会保存在protocol buffer中,相当于java中的List。
枚举:只能用指定的常量集中的一个值作为其值,枚举常量必须在32位整型值的范围内(不推荐使用负数),用分号间隔,而不是Java中的逗号。
2.几种限定符的基本规则。
在每个消息中必须至少留有一个required类型的字段。
每个消息中可以包含0个或多个optional类型的字段。
repeated表示的字段可以包含0个或多个数据。需要说明的是,这一点有别于Java中的数组,因为java的数组必须包含至少一个元素。
兼容性: sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容;string和bytes是兼容的——只要bytes是有效的UTF-8编码;嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本; fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
3.分配标识号
在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变;
[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号;
最小的标识号可以从1开始,最大到2^29 – 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。
4.添加注释
向.proto文件添加注释,可以使用java风格的双斜杠(//) 语法格式。
5.嵌套类型
你可以在其他消息类型中定义、使用消息类型,也可以将消息嵌套任意多层,组的特性已被启用,可以使用嵌套消息类型来代替组。
6.定义服务(Service)
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。
所有service类都必须实现Service接口,它提供了一种用来调用具体方法的方式,即在编译期不需要知道方法名及它的输入、输出类型。在服务器端,通过服务注册它可以被用来实现一个RPC Server
7.扩展
通过扩展,可以将一个范围内的字段标识号声明为可被第三方扩展所用。然后,其他人就可以在他们自己的.proto文件中为该消息类型声明新的字段,而不必去编辑原始文件了,也可以在另一个类型(嵌套类型)的范围内声明扩展。
8.消息升级原则
不要更改任何已有的字段的数值标识
optional和repeated限定符也是相互兼容的。
非required的字段可以移除,但最好重命名你要删除的字段。
在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用
如果打算在原有消息协议中添加新的字段,同时还要保证旧版本的程序能够正常读取或写入,那么对于新添加的字段必须是optional或repeated。因为旧版本程序无法读取或写入新增的required限定符的字段。
9.使用步骤
下载protobuf.exe和protobuf.jar文件(java版),下载地址:protobuf下载
定义用于消息文件.proto
使用protobuf的编译器编译消息文件
使用编译好对应语言的类文件进行消息的序列化与反序列化
上面说了那么多protobuf的用法,你是不是有点懵,哈哈!其实,我也有点懵逼,下面我就来举一个比较全面的例子来解释protobuf在实际中到底该怎么用。
例1:
(1)创建User.proto文件
option java_package="other";
option java_outer_classname="UserProto";
message User{
required string name = 1;
required int32 password = 2;
optional int64 phone = 3;
repeated string hobby = 4;
optional string sex = 5;
optional UserStatus user = 6[default = OFF_LINE];
optional Address address = 7;
enum UserStatus {
OFF_LINE = 0; //离线用户
ON_LINE = 1; //在线用户
}
message Address {
optional string province = 1;
required int32 postcode = 2;
}
}
分析:
java_package是文件级别的选项,用于指定包名,比如说上例中的包名为other,与此同时,生成的Java文件也将会自动存放到指定输出目录下的other子目录中。
java_outer_classname是也是文件级别的选项,用于指定生成Java代码的外部类名称。如果没有指定该选项,Java代码的外部类名称为当前文件的文件名部分,同时还要将文件名转换为驼峰格式
还有一种选项optimize_for,它是文件级别的选项,一般很少用,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。
name 和password都是必须要指定值的字段, 而phone和sex是可选的,你可以指定值,也可以不管它,hobby相当于java中的ArrayList,可以指定多个值;
UserStatus也是可选的,但是如果你要指定值,只有两个值,OFF_LINE和ON_LINE,如果不指定值,那么后面的default字段就起作用了,默认是OFF_LINE
Address是可选的,它属于嵌套类型,相当于java中的内部类,里面有两个字段,其中postcode必须指定值,而province是可选的。
我分配的标识号从1到7,注意标识号一定不能重复,若包含枚举和嵌套类型,则他们里面的字段标识号不能重复。
(2)打开命令提示符窗口,进入到存放proto.exe和User.proto文件的文件夹中,执行命令
分析:User.proto就是你的proto文件的名字,–java_out=./ 就是生成的,java文件存放的目录,我没指定,就默认存在本目录下。
注:对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
例2:生成的UserProto类
package other;
public final class UserProto {
private UserProto() {}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) {
}
//中间省略很多行 ......
com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
}, assigner);
}
// @@protoc_insertion_point(outer_class_scope)
}
(3)为了验证生成的.java文件能否成功序列化,下面我写了一个测试文件:
例3:测试类
public class Testprotobuf {
public byte[] encode() {
UserProto.User.Address.Builder bu = UserProto.User.Address.newBuilder();
bu.setProvince("shanxi");
bu.setPostcode(710055);
UserProto.User.Address address = bu.build();
UserProto.User.Builder builder = UserProto.User.newBuilder();
builder.setName("Taylor");
builder.setPassword(123456);
builder.setPhone(82927222);
builder.setAddress(address);
builder.setSex("Woman");
List<String> list = new ArrayList<String>();
list.add("basketball");
list.add("badminton");
list.add("soccer");
builder.addAllHobby(list);
byte[] bytes = builder.build().toByteArray();
return bytes;
}
private static UserProto.User decode(byte[] body) throws InvalidProtocolBufferException {
return UserProto.User.parseFrom(body);
}
public static void main(String[] args) throws InvalidProtocolBufferException {
Testprotobuf t = new Testprotobuf();
UserProto.User user = t.decode(t.encode());
System.out.println("解码后信息: "+user);
System.out.println("状态 :"+user.getUser());
}
}
运行结果:
解码后信息: name: "Taylor"
password: 123456
phone: 82927222
hobby: "basketball"
hobby: "badminton"
hobby: "soccer"
sex: "Woman"
address {
province: "shanxi"
postcode: 710055
}
状态 :OFF_LINE
分析:从结果可以看出,我在编码中指定的内容,解码后都输出来了,并且都准确,并没有乱码问题,值得一提的是,我在编码中并没有指定用户的状态,最后为什么还获取到了值,这是因为我在 User.proto 文件中指定了它的默认值。
讲了这么多那到底在Netty中该如何使用protobuf进行编解码呢,接下来我写一个例子来引导你如何使用
例4:protobuf在Netty中的使用
分析:就是将childHandler中的处理类改一下,其他都差不多,我就不在这里贴全部的代码了!
ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类
ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的UserProto.java文件中的解码类
ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段,用于标志这个消息的长度的类
ProtobufEncoder 是编码类
如果你想了解更底层的原理,请参考:protobuf编码详解
本人才疏学浅,若有错,请指出,谢谢!
如果你有更好的建议,可以留言我们一起讨论,共同进步!
衷心的感谢您能耐心的读完本篇博文。