Protobuf 全称:Google Protocol Buffers,由谷歌开源而来,经谷歌内部测试使用。它将数据结构以 .proto 文件进行描述,通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的方法和属性。

# 一、 Protocol 的特点

【1】在谷歌内部长期使用,产品成熟度高;
【2】高效的编解码性能,编码后的消息更小,有利于存储和传输;
【3】语言无关、平台无关、扩展性好
【4】官方支持 JavaC++C#PythonGoDart

Protobuf 使用二进制编码,在空间和性能上相对于 XML具有很大的优势。尽量 XML的可读性和可扩展性非常好,也非常适合描述数据结构,但是 XML 解析的时间开销和 XML为了可读性而牺牲的空间开销都非常大,因此不适合做高性能的通信协议。

Protobuf 的数据描述文件和代码生成机制(跨语言的编解码框架,都具有此功能),优点如下:
 ■ 文本化的数据结构描述语言,可以实现语言和平台无关,特别适合异构系统间的集成;
 ■ 通过标识字段的顺序,可以实现协议的前向兼容;
 ■ 自动代码生成,不需要手工编写同样数据结构的 C++Java 版本;
 ■ 方便后续的管理和维护。相当于代码,结构化的文档更容易管理和维护。

Protobuf 的编解码性能远远高于JSON<Serializable<hession2<hession1<XStream<hession2压缩(性能有高到底)等序列化框架的序列化和反序列化,这也是很多 RPC 框架选用 protobuf 做编解码框架的原因。

# 二、Protobuf 开发环境搭建

【1】首先下载 Protobuf 的最新 Windown 版本:网站地址如下:链接 (opens new window)

protoc-3.9.1-win32.zip
protoc-3.9.1-win64.zip
1
2

下载后对其解压:进入包含 protoc.exe 的文件目录,配置其环境变量;protoc.exe 工具主要根据 .proto 文件生成代码。

官网对 java 编写 .proto 文件,详细说明地址:链接 (opens new window)

下面我们定义一个 person.proto 数据文件。如下: 注释写在#号后,实际不能这么操作。此处为方便理解:

#类似于c++或java。检查一下文件的每一部分,看看它的作用。
syntax = "proto2";
#以包声明开始,这有助于防止不同项目之间的命名冲突
package tutorial;
#在java中,包名用作java包,除非您已经显式地指定了java_包,如我们这里所述。
#即使您确实提供了一个java_包,您也应该定义一个普通包,以避免在协议缓冲区名称空间和非java语言中发生名称冲突。
#如果不提供此属性,以package 为准
#java_package指定生成的类的java包名。
#如果您没有显式地指定它,那么它只匹配包声明给出的包名,但是这些名称通常不适合Java包名(因为它们通常不以域名开头)
option java_package = "com.example.tutorial";
#java_outer_class name选项定义类名,该类名应包含此文件中的所有类。
#如果没有显式地给出java_outer_类名,则将通过将文件名转换为camel case来生成它。
#例如,“my_proto.proto”在默认情况下将使用“myProto”作为外部类名。利用驼峰命名法。
option java_outer_classname = "AddressBookProtos";

#开始定义消息,相当于内部类 Person
message Person {
  # required 表示必须字段,1是序号不是赋值的意思,表示唯一的标记。
  # 建议不要使用 required 而使用optional 因为当后期将 required 修改为 optional 会有问题。
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

【2】通过 protoc.exe 命令行生成 Java 代码,命令如下:[ --java_out=生成 *.java 文件的存放路径,我所在的目录正是存放person.proto 文件的目录 ]没有任何错误就说明生成成功。

E:\learnWorkspacesDesign\netty_learn\src\protobuf>protoc.exe --java_out=..\main\java person.proto
1

【3】查看生成的目标文件:或者在外面生成好,拷贝进来也行。建议不要对生成的文件做任何修改。我们发现代码编译出错,原因是因为少了 protobufjar 包:
粘包
引入 protobuf-java 相关的 jar 包,如下:

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.9.1</version>
</dependency>
1
2
3
4
5

到此为止,Protobuf 开发环境已经搭建完毕,接下来进行示例展示。

# 三、Protobuf 编解码开发

Protobuf 的类库使用比较简单,下面通过对 AddressBookProtos 编解码来介绍 Protobuf 的使用:由于 Protobuf 支持复杂 POJO 对象编解码,所以代码都是通过工具自动生成,相比于传统的 POJO 对象的赋值操作,其使用略微复杂一些。Protobuf 的编解码接口非常简单和实用,但是功能和性能却非常强大,这也是它流行的一个重要原因。

public class TestAddressBookProtos {
    public static void main(String[] args) throws InvalidProtocolBufferException {
        AddressBookProtos.Person person = createSubscribeReq();
        /*
         * After decode:name: "ZhengZhaoXiang"
         * id: 1
         * email: "1179278531@qq.com"
         */
        System.out.printf("Before encode :"+person.toString());
        AddressBookProtos.Person personObj = decode(encode(person));
        /*
         * After decode:name: "ZhengZhaoXiang"
         * id: 1
         * email: "1179278531@qq.com"
         */
        System.out.printf("After decode:"+person.toString());
        //输出: Assert equal:true
        System.out.printf("Assert equal:"+person.equals(personObj));
    }

    //编码 通过调用 AddressBookProtos.Person 实例的 toByteArray 即可将 Person 编码为 byte 数组。
    private static byte[] encode(AddressBookProtos.Person person){
        return person.toByteArray();
    }

    //解码  还可以解码流数据  parseFrom(InputStream i);
    private static AddressBookProtos.Person decode(byte[] body) throws InvalidProtocolBufferException {
        return AddressBookProtos.Person.parseFrom(body);
    }

    //创建一个 person 对象
    private static AddressBookProtos.Person createSubscribeReq(){
        // 通过 AddressBookProtos.Person 的 newBuilder() 静态方法创建 Builder 实例
        // 通过 Builder 构建器对 Person 的属性进行设置,对于集合类型,通过addAllXXX()方法将值设置到属性中。
        return AddressBookProtos.Person.newBuilder()
                .setId(1).setName("ZhengZhaoXiang").setEmail("1179278531@qq.com").build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 四、NettyProtobuf 服务端开发

【1】标准的服务端:主要区别在于 childHandler 方法中的 PersonChannelInitializer 类的内容。

public class PersonServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    //主要查看 PersonChannelInitializer 内容
                    .childHandler(new PersonChannelInitializer());
            ChannelFuture future = bootstrap.bind(8899).sync();
            future.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

【2】PersonChannelInitializer 内容展示:重点关注自定义 handlerPersonHandler

public class PersonChannelInitializer extends ChannelInitializer{
    @Override
    protected void initChannel(Channel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        //主要用于半包处理
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //解码器,参数 com.google.protobuf.MessageLite 实际上是告诉 ProtobufDecoder 解码的目标类
        pipeline.addLast(new ProtobufDecoder(AddressBookProtos.Person.getDefaultInstance()));
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        pipeline.addLast(new StringEncoder());
        //自定义handler
        pipeline.addLast(new PersonHandler());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

【3】自定义 PersonHandler 的内容如下:由于 ProtobufDecoder 已经对消息进行了自动解码,因此接收到的 Person 消息可以直接使用。对用户进行校验,校验通过后构造应答消息返回给客户端,由于使用了 StringEncoder 因此不需要手工编码。

public class PersonHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
        AddressBookProtos.Person person = (AddressBookProtos.Person)msg;
        System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
        System.out.printf("服务端收到的消息    "+person);
        channelHandlerContext.writeAndFlush("from client"+ LocalDateTime.now());
    }

    @Override
    public void channelActive(ChannelHandlerContext channelHandlerContext){
        channelHandlerContext.writeAndFlush("来着服务端的问候:Active"+"\r\n");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
        e.printStackTrace();
        channelHandlerContext.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 五、Netty 的 Protobuf 客户端开发

【1】客户端:主要区别在于 childHandler 方法中的 PersonClientInitializer 类的内容。

public class PersonClient {
    public static void main(String[] args) throws Exception{
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(workerGroup).channel(NioSocketChannel.class)
                    .handler(new PersonClientInitializer());
            ChannelFuture future = bootstrap.connect("127.0.0.1",8899).sync();
            future.channel().closeFuture().sync();
        }finally {
            workerGroup.shutdownGracefully();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

【2】PersonClientInitializer 内容展示:重点关注自定义 handlerPersonClientHandler

public class PersonClientInitializer extends ChannelInitializer{
    @Override
    protected void initChannel(Channel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        pipeline.addLast(new StringDecoder());
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        pipeline.addLast(new ProtobufEncoder());
        pipeline.addLast(new PersonClientHandler());
    }
}
1
2
3
4
5
6
7
8
9
10
11

【3】自定义 PersonClientHandler 的内容如下:

public class PersonClientHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
        System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
        System.out.printf("客户端收到的消息:   "+"\r\n" + msg);
    }

    @Override
    public void channelActive(ChannelHandlerContext channelHandlerContext){
        AddressBookProtos.Person person = AddressBookProtos.Person.newBuilder().setId(1)
                .setName("zhengzhaoxiang")
                .setEmail("1179278531@qq.com").build();
        channelHandlerContext.channel().writeAndFlush(person);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
        e.printStackTrace();
        channelHandlerContext.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 六、测试

启动服务端——>启动客户端,运行结果如下: 【1】服务端结果展示:

/127.0.0.1:57595服务端收到的消息   name: "zhengzhaoxiang"
id: 1
email: "1179278531qq.com"
1
2
3

【2】客户端结果展示:

Connected to the target VM, address: '127.0.0.1:57572', transport: 'socket'
/127.0.0.1:8899客户端收到的消息:
来着服务端的问候:Active
/127.0.0.1:8899客户端收到的消息:
from client2019-09-21T19:20:14.977
1
2
3
4
5

# 七、问题

.proto 中存在多个 message 时,在解码 ProtobufDecode(目标对象)中,添加的目标对象不唯一,会根据情况进行变化的问题及解决方案。

【1】.proto 文件内容如下:包含多个 message 对象。oneof 关键字表示:多个可选项,但允许选择一个。设置的新值会替换掉旧值。

syntax = "proto2";

package tutorial;

option java_package = "com.protobuf";
option java_outer_classname = "AddressBookProtos";

message myMessage {
  enum data {
    personType = 1;
    dogType = 2;
    pigType = 3;
  }
  
  required string type = 1;
  oneof zoo {
    Person person = 2;
    Dog dog = 3;
    Pig pig =4;
  }
}

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
}

message Dog {
  optional string name = 1;
}

message Pig {
  optional string name = 1;
  optional int32 price = 2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

【2】编辑码出的问题,便可以修改为最外层的 myMessage 对象,服务端解码设置如下:

pipeline.addLast(new ProtobufDecoder(AddressBookProtos.myMessage.getDefaultInstance()));
1

【3】客户端发送发送消息,内容如下:需要什么对象,就往 oneof 中传入目标对象即可。

@Override
public void channelActive(ChannelHandlerContext channelHandlerContext){
    int random = new Random().nextInt(3);
    AddressBookProtos.myMessage message = null;
    if(random == AddressBookProtos.myMessage.data.personType_VALUE){
        message = AddressBookProtos.myMessage.newBuilder()
                .setType("1").setPerson(AddressBookProtos.Person.newBuilder()
                .setId(1).setName("zheng").setEmail("117278531@qq.com").build()).build();
    }else if(random == AddressBookProtos.myMessage.data.dogType_VALUE){
        message = AddressBookProtos.myMessage.newBuilder()
                .setType("2").setDog(AddressBookProtos.Dog.newBuilder()
                        .setName("一条狗").build()).build();
    }else{
        message = AddressBookProtos.myMessage.newBuilder()
                .setType("3").setPig(AddressBookProtos.Pig.newBuilder()
                        .setName("一只猪").setPrice(20).build()).build();
    }
    channelHandlerContext.channel().writeAndFlush(message);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

【4】服务端接受客户端的消息,根据 type 的值判断需要解析的数据信息,具体内容如下:

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
    AddressBookProtos.myMessage message = (AddressBookProtos.myMessage)msg;
    if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.personType_VALUE)){
        System.out.printf("服务端收到的消息    "+message.getPerson().toString());
    }else if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.dogType_VALUE)){
        System.out.printf("服务端收到的消息    "+message.getDog().getName());
    }else{
        System.out.printf("服务端收到的消息    "+message.getPig().getName()+"\r\n"+message.getPig().getPrice());
    }
}
1
2
3
4
5
6
7
8
9
10
11

【5】不断重启客户端,会根据随机数得到不同的结果,如下:

//第一次输入结果展示:
/*服务端收到的消息    name: "zheng"
id: 1
email: "117278531@qq.com"*/

//第三次输入结果展示:
/*服务端收到的消息    一条狗*/

//第四次输入结果展示:
/*服务端收到的消息    一只猪
20*/
1
2
3
4
5
6
7
8
9
10
11
(adsbygoogle = window.adsbygoogle || []).push({});