【Netty基础】protobuf详解

前言

我在上篇博文中简单介绍了一下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的数据类型

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

protobuf的用法

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的用法,你是不是有点懵,哈哈!其实,我也有点懵逼,下面我就来举一个比较全面的例子来解释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。

    • SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。
    • CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。
    • LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。
    • 注:对于LITE_MESSAGE选项而言,其生成的代码均将继承自MessageLite,而非Message。
  • 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

讲了这么多那到底在Netty中该如何使用protobuf进行编解码呢,接下来我写一个例子来引导你如何使用

例4:protobuf在Netty中的使用

分析:就是将childHandler中的处理类改一下,其他都差不多,我就不在这里贴全部的代码了!

  • ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类

  • ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的UserProto.java文件中的解码类

  • ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段,用于标志这个消息的长度的类

  • ProtobufEncoder 是编码类

如果你想了解更底层的原理,请参考:protobuf编码详解


本人才疏学浅,若有错,请指出,谢谢!
如果你有更好的建议,可以留言我们一起讨论,共同进步!
衷心的感谢您能耐心的读完本篇博文。

你可能感兴趣的:(框架,protobuf)