一、PB介绍
1.1 为甚需要PB ,它解决了什么问题?
网络上传输的数据都是二进制字节数据,说白了,就是一串的0 和 1,往硬件层面上说,是 高低电压(OR 光)脉冲。
从Java 对象装换成 二进制字节数据,涉及到一个编码格式的问题。最熟悉的场景是我们将一个Java字符串 ,通过UTF-8 转换为一个二进制字节数组传输,但除了 使用UTF-8编码(解码亦然),还有没有其他方式呢?
Netty 自身也提供了一些codec,比如
StringEncoder (StringDecoder)
:对字符串编解码ObjectEncoder(ObjectDecoder)
:对Java对象编解码- 等等
Netty自带的 ObjectEncoder(ObjectDecoder)
可以用来实现POJO对象或各种业务对象的编解码,本质上还是Java序列化技术,java 序列化本身效率不高,而且还有一些硬伤:
- 不能跨语言
- 序列化后的体积大,是二进制编码的 5 倍+
- 序列化性能低
PB的出现就是解决这些硬伤的:
PB 是一种结构化数据存储格式,适合结构化数据串行化,或者说 序列化,适合RPC和存储。现在很多公司,都从HTTP+JSON–> RPC+PB
1.2 如何使用PB
使用示意图:
使用前,最好阅读下 PB的文档:
1.3 demo
案例1:如何发一种数据类型
使用 PB编解码,客户端Student对象发个简单消息给服务端,服务端能接收,能正常通信即可;
// PB描述文件内容
syntax="proto3";
option java_outer_classname="StudentPOJO"; //生成外部类名、同时也是类文件名
//PB使用 message 管理数据
message Student{ //会在 StudentPOJO (外部类)生成一个内部类 Student --> 即真正的POJO对象
int32 id=1; //Student中一个属性,名字就是 id ,类型是 Int32 对应到Java的 int
// 这里的 1 注意不是值,而是序号!!!
string name=2;
}
//客户端 核心代码也就一个handler
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 注意: 这个 编码也是在第一行;并不是在最后一行
ch.pipeline().addLast("pbEncoder", new ProtobufEncoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setName("justinwins")
.setId(1024).build();
ctx.writeAndFlush(student);
}
});
}
});
//服务端 核心代码也就一个handler
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
.group(boss, worker)
.option(ChannelOption.SO_BACKLOG, 128)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
// 注意:解码器的添加需要指定是哪种类型
ch.pipeline().addLast("pbDecoder",
new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
StudentPOJO.Student student = (StudentPOJO.Student) msg;
System.out.println("收到客户端的数据id " + student.getId() + ",name " + student.getName());
}
});
}
}).childOption(ChannelOption.SO_KEEPALIVE, true);
案例2:如何发很多种数据类型
上面的案例中我们只能收发一种数据类型,实际生产过程中,肯定不止使用一种,难道每个POJO都大张旗鼓地搞个proto文件?
这就要从 .proto
的高级语法说起了。
举个需求:客户端随机发送Boss OR Worker 这两个实例中的一个;要求服务端能够接收到
// .proto文件如下
syntax="proto3";
option optimize_for=SPEED;//加快解析速度
option java_outer_classname="MyDataInfo";
message MyMessage{
// 定义一个枚举类型
enum DataType{
BossType=0; // 枚举类型是从 0 开始编号的
WorkerType=1;
}
// 用data_type来标识传的是哪一个枚举。注意,这里的 =1 并不是指定为了WorkerType,
// 它只是很纯粹的表示顺序而已
DataType data_type=1;
// oneof 表示每次枚举类型只能出现其中一个。 dataBody 就是随意起一个名字而已。
oneof dataBody{
Boss boss=2;
Worker worker=3;
}
}
message Boss{
int32 id=1;
string name=2;
}
message Worker{
int32 age=1;
string name=2;
}
// 核心server 代码如下
bootstrap.group(boss, worker).option(ChannelOption.SO_BACKLOG, 128).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
// 注意:解码器的添加需要指定是哪种类型
ch.pipeline().addLast("pbDecoder",
new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
// 使用 SimpleChannelInboundHandler ,省去了强转的麻烦
ch.pipeline().addLast(new SimpleChannelInboundHandler<MyDataInfo.MyMessage>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg)
throws Exception {
if (msg.getDataType().equals(MyDataInfo.MyMessage.DataType.BossType)) {
MyDataInfo.Boss msgBoss = msg.getBoss();
System.out.println("接收到Boss:" + msgBoss.getName());
} else {
MyDataInfo.Worker msgWorker = msg.getWorker();
System.out.println("接收到Worker:" + msgWorker.getName());
}
}
});
}
}).childOption(ChannelOption.SO_KEEPALIVE, true);
// 核心client代码如下
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 注意: 这个 编码也是在第一行;并不是在最后一行
ch.pipeline().addLast("pbEncoder", new ProtobufEncoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//随机发送一个 Boss OR Worker 实例
int index = (int)(Math.random() * 10) % 2;
if (index == 0) {
MyDataInfo.MyMessage bossB = MyDataInfo.MyMessage.newBuilder()
.setDataType(MyDataInfo.MyMessage.DataType.BossType)
.setBoss(MyDataInfo.Boss.newBuilder().setId(1024).setName("bossB").build())
.build();
ctx.writeAndFlush(bossB);
} else {
MyDataInfo.MyMessage.Builder workerW = MyDataInfo.MyMessage.newBuilder()
.setDataType(MyDataInfo.MyMessage.DataType.WorkerType).setWorker(
MyDataInfo.Worker.newBuilder().setAge(10).setName("workerW").build());
ctx.writeAndFlush(workerW);
}
}
});
}
});
1.4 遗留问题
PB 使用 message 来管理数据,如何理解?