Netty-11-ProtoBuf

Netty-11-ProtoBuf

前言

  • 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据的时候就需要编码,接收数据时候就需要解码
  • codec(编解码器)的组成部分有两个
    • decoder(解码器) : 负责把字节码数据转换成业务逻辑
    • encoder(编码器) : 负责把业务数据转换成字节码数据

mark

1. Netty 提供的编解码机制

  • Netty 本身的编解码器的机制和问题分析(为什么要引入protobuf ?)
  • Netty 自身提供了一些 codec(编解码器)
    • Netty 提供的编码器 StringEncoder,对字符串数据进行编码 ObjectEncoder,对Java对象进行编码…
    • Netty 提供的解码器 StringDecoder,对字符串数据进行解码 ObjectDecoder,对 Java 对象进行解码…
  • Netty 本身自带的 ObjectDecoderObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,底层使用的仍是Java序列化技术,而Java序列化技术本身效率就不高,存在如下问题
    • 无法跨语言
    • 序列化后的体积太大,是二进制编码的5倍多
    • 序列化性能太低

于是引出了新的解决方案(Google Protobuf)

2. ProtoBuf 简介

1. Google ProtoBuf 参考文档

2. 简介概述

  • 首先,ProtoBuf 是用来将对象进行序列化的,相类似的技术还有Json 序列化等等,它是一种高效的结构化数据存储格式,
  • 可以用于结构化数据串行化(序列化)。它很适合做数据存储或者RPC(远程工程调用)数据交换格式 (目前很多公司 http + json || tcp + protobuf
  • ProtoBuf 是以 message的方式来管理数据的

3. 优点:

  • 支持跨平台跨语言 【即客户端可以使用不同的语言编写】
    • 支持目前绝大多数语言,例如 C++、C#、Java、python 等
  • 使用 protobuf 编译器能自动生成代码
  • protobuf 是将 类的定义使用.proto 文件进行描述
    • 说明,在idea 中编写 .proto 文件时,会自动提示是否下载 .proto 编写插件,可以让语法高亮
    • 然后通过 proto.exe 编译器根据 .proto 自动的生成java 文件

使用示意图 :

mark

3. Proto 文件格式

  • 首先我们需要在.proto文件中定义好实体及他们的属性,再进行编译成java对象为我们所用。下面将介绍proto文件的写法。
  1. 文件头
  • 像写java需要写package包名一样,.proto文件也要写一些文件的全局属性,主要用于将.proto文件编译成Java文件。
实例 介绍
syntax="proto3"; 声明使用到的protobuf的版本
optimize_for=SPEED; 表示
java_package="com.mical.netty.pojo"; 表示生成Java对象所在包名
java_outer_classname="MyWorker"; 表示生成的Java对象的外部类名
  • 我们一般将这些代码写在proto文件的开头,以表明生成Java对象的相关文件属性。
  1. 定义类和属性
1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3"; //版本
option optimize_for = SPEED; //加快解析
option java_outer_classname = "MyDataInfo"; //生成的外部类名,同时也是文件名

message Student { //会在StudentPojo 外部类生成一个内部类Student,他是真正发送的pojo对象
int32 id = 1; //Student类中有一个属性名字为ID,类型为int32(protobuf类型),1表示序号,不是值
string name = 2;
}

enum DateType {
StudentType = 0; //在proto3中,要求enum的编号从0开始
WorkerType = 1;
}
  • 如上图所示,我们在文件中不但声明了protobuf的版本,还声明了生成java对象的类名。当生成java对象后,MyDataInfo将是对象的类名,同时,它使用message声明了Student这个内部类,使用enum声明了DataType这个内部枚举类。就像下面这个样子

  • messag:声明类。

  • enum:声明枚举类。

1
2
3
4
public final class MyDataInfo {
public static final class Student { }
public enum DataType { }
}

然后需要注意的是,protobuf中的变量类型和其他语言的声明有所不同。下面是类型的对照表

.proto类型 java类型 C++类型 备注
double double double
float float float
int32 int int32 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint32。
int64 long int64 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint64。
unit32 int[1] unit32 总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。
unit64 long[1] unit64 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。
sint32 int int32 使用可变长编码方式。有符号的整型值。编码时比通常的int32高效。
sint64 long int64 使用可变长编码方式。有符号的整型值。编码时比通常的int64高效。
fixed32 int[1] unit32
fixed64 long[1] unit64 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。
sfixed32 int int32 总是4个字节。
sfixed64 long int64 总是8个字节。
bool boolean bool
string String string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。
bytes ByteString string 可能包含任意顺序的字节数据
  • 类型关注之后,我们看到代码中string name = 2,它并不是给name这个变量赋值,而是给它标号。每个类都需要给其中的变量标号,且需要注意的是类的标号是从1开始的,枚举的标号是从0开始的。
  1. 复杂对象
  • 当我们需要统一发送对象和接受对象时,就需要使用一个对象将其他所有对象进行包装,再获取里面的某一类对象。
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
syntax = "proto3"; //版本
option optimize_for = SPEED; //加快解析
option java_outer_classname = "MyDataInfo"; //生成的外部类名,同时也是文件名

message MyMessage {
//定义一个枚举类型
enum DateType {
StudentType = 0; //在proto3中,要求enum的编号从0开始
WorkerType = 1;
}

//用data_type来标识传的是哪一个枚举类型
DateType data_type = 1;

//标识每次枚举类型最多只能出现其中的一个类型,节省空间
oneof dataBody {
Student stuent = 2;
Worker worker = 4;
}
}

message Student { //会在StudentPojo 外部类生成一个内部类Student,他是真正发送的pojo对象
int32 id = 1; //Student类中有一个属性名字为ID,类型为int32(protobuf类型),1表示序号,不是值
string name = 2;
}
message Worker {
string name = 1;
int32 age = 2;
}
  • 这里面我们定义了MyMessageStudentWorker三个对象
  • MyMessage里面持有了一个枚举类DataType和,StudentWorker这两个类对象中的其中一个。
  • 这样设计的目的是什么呢?当我们在发送对象时,设置MyMessage里面的对象的同时就可以给枚举赋值,这样当我们接收对象时,就可以根据枚举判断我们接受到哪个实例类了。

4. Netty ProtoBuf编解码器

4.1 发送端

  1. 需要给发送端的pipeline添加编码器:ProtobufEncoder
1
2
3
4
5
6
7
8
9
10
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast(new ProtoClientHandler());
}
});
  1. 在发送时,如何构造一个具体对象呢?以上面复杂对象为例,我们主要构造的是MyMessage对象,设置里面的枚举属性,和对应的对象。
1
2
3
4
5
6
7
8
9
MyDataInfo.MyMessage build = MyDataInfo.MyMessage.
newBuilder().
setDataType(MyDataInfo.MyMessage.DateType.StudentType)
.setStuent(MyDataInfo.Student
.newBuilder()
.setId(5)
.setName("王五")
.build())
.build();

4.2 接收端

  1. 需要在接收端添加解码器:ProtobufDecoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//需要指定对哪种对象进行解码
pipeline.addLast("decoder", new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
pipeline.addLast(new ProtoServerHandler());
}
})
  1. 在接收对象时,我们就可以根据枚举变量去获取实例对象了。
1
2
3
4
5
6
7
8
9
10
11
12
13
MyDataInfo.MyMessage message = (MyDataInfo.MyMessage) msg;
MyDataInfo.MyMessage.DateType dataType = message.getDataType();

switch (dataType) {
case StudentType:
MyDataInfo.Student student = message.getStuent();
System.out.println("学生Id = " + student.getId() + student.getName());
case WorkerType:
MyDataInfo.Worker worker = message.getWorker();
System.out.println("工人:name = " + worker.getName() + worker.getAge());
case UNRECOGNIZED:
System.out.println("输入的类型不正确");
}

本文档整理自 尚硅谷韩顺平Netty 相关课程。

参考博客 : https://dongzl.github.io/netty-handbook/#/README

idea 使用 protobuf 问题 : https://www.cnblogs.com/liugh/p/7505533.html

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信