编码与解码:
1.编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据就需要编码,接受数据就需要解码。
2. codec(编解码器)的组成部分有两个:decoder(解码器)和encoder(编码器)。encoder负责把业务数据转换成字节码数据,decoder负责把字节码数据转换成业务数据。
netty自带的编解码器
netty提供的编码器:
stringEncoder:对字符串数据进行编码
objectEncoder:对java对象进行编码
netty提供的解码器:
StringDecoder :对字符串进行解码
ObjectDecoder :对java对象进行解码
netty自带的编解码器的问题以及解决方案
因为Netty自带的编解码器底层使用的是java 序列化技术,而java的序列化技术本身效率就不高,存在以下问题:
1.无法跨语言
2.序列化后的体积太大,是二进制编码的5倍多
3.序列化性能太低
解决方案:
Google–Protobuf
Protobuf
1.Protobuf 是Google发布的开源项目,全程Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化,它很适合做数据存储活RPC数据交换格式。
2. 参考文档:https://developers.google.com/protocol-buffers/docs/proto
3. Protobuf是以message的方式来管理数据的。
4. 支持跨平台,跨语言,即客户端和服务端可以是不同的语言编写的。
5. 高性能,高可靠性。
6. 使用 protobuf编译器能自动生成代码,Protobuf 是将类的定义使用.proto文件进行描述,说明,在idea中编写.proto文件时,会自动提示是否下载.ptotot编写的插件,可以让语法高亮。
7. 然后通过protoc.exe编译器根据.proto自动生成.java文件
8. Protobuf示意图:
Protobuf使用案例:
需求:我们通过Protobuf,从客户端发送一个Student对象到服务器端
0.我们使用的是Maven项目,导入Protobuf的依赖
注意我们这次使用的版本为3.6.1
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.6.1</version>
</dependency>
1.新建一个.proto文件
new — File 这里我们叫做 Student.proto
当我们第一次创建这个文件的时候,IDEA会提示我们安装一个插件,我们安装即可,确定安装成功后,需要重启我们的IDEA
然后发现创建的Student.proto图标变了 说明安装成功
2. 编写我们的Student.proto文件
syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO"; // 生成外部类名,同时也是文件名
// protoBuf 是以message管理数据
message Student { // 会在StudentPOJO 外部类生成一个内部类Student,他才是真正发送的对象
int32 id = 1 ; // Student 类中有一个属性,名字为id int32 对应java中的int 1表示属性序号,不是值
string name = 2;
}
syntax : 我们proto的版本
option java_outer_classname :生成的外部类名称,即自定生成的类名
message :是以message管理数据。
注意:名字为id int32 对应java中的int 1表示属性序号,不是值,而且这里的字符串对应string,第一个字母是小写
3. 通过protoc3.6.1.exe自动生成java文件
protoc3.6.1.exe下载地址:
https://download.csdn.net/download/weixin_43326401/12151500
将我们的Student.proto 放在与protoc3.6.1.exe的同一目录,并用linux生成java文件
然后cmd 运行,进入protoc3.6.1.exe所在的文件夹,输入命令 protoc3.6.1.exe --java_out=. Student.proto
如果窗口没报错,就会在同一目录下生成一个java文件:
然后我们将生成的java文件复制进我们的项目当中
如果该类中有报错:
我们需要 file—Project Structure,这里我使用的是jdk1.8,所以调成对应的版本
然后我们file ----setting
也需要改成对应的jdk版本,然后就不报错了
4.准备工作就OK了 编写服务端
package com.jym.codec;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
/**
* @program: NettyPro
* @description: Netty服务端练习
* @author: jym
* @create: 2020/02/07
*/
public class JymNettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建bossGroup,workGroup
// 1.创建两个线程组bossGroup和workGroup
// 2.bossGroup只是处理连接请求,真正的和客户端处理,会交给workGroup完成
// 3.两个都是无限循环
// 4.bossGroup和workGroup还有的子线程(NioEventLoop)的个数,默认实际CPU的核数*2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
// 创建服务器端启动的对象,可以配置启动参数
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 使用链式编程来进行设置
// 设置两个线程组
serverBootstrap.group(bossGroup,workGroup)
// 使用NioServerSocketChannel作为服务器的通道实现
.channel(NioServerSocketChannel.class)
// 设置线程队列得到连接个数
.option(ChannelOption.SO_BACKLOG,128)
// 设置保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE,true)
// 给我们的workGroup的EventLoop对应的管道设置处理器
// 创建一个通道测试对象
.childHandler(new ChannelInitializer<SocketChannel>() {
// 给pipeline设置处理区
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 解码 指定对哪种对象进行解码
pipeline.addLast("decoder",new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
pipeline.addLast(new JymNettyServerHandler());
}
});
System.out.println("服务器 is ready");
// 绑定端口并且同步,生成一个ChannelFuture对象,启动服务器
ChannelFuture sync = serverBootstrap.bind(6668).sync();
// 对关闭通道进行监听
sync.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
这里特殊说明一下:
ProtobufDecoder:构造方法里传入的,是指定对哪种对象进行解码,因为我们是对Student进行解码,所以是StudentPOJO.Student.getDefaultInstance()
5.服务端自定义handler:
package com.jym.codec;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @program: NettyPro
* @description: 1.自定义handler,需要继承netty规定好的某个HandlerAdapter(规范)
* @description: 2.这时我们的自定义handler,才能称为一个handler
* @author: jym
* @create: 2020/02/08
*/
public class JymNettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数据实际(这里我们可以读取从客户端发来的消息)
* 1.ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道,地址
* 2.Object msg: 客户端发送的数据,默认是object的形式
*/
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
// 读取客户端发送的studentPojo.student
StudentPOJO.Student student = (StudentPOJO.Student) msg;
System.out.println("客户端发送的数据:"+student.getId() + student.getName());
}
/**
* 数据读取完毕
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// writeAndFlush 是write+flush,将数据写入到缓存,并刷新
// 一般讲,我们对发送的数据进行一个编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
}
/**
* 处理异常,一般是关闭连接
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
6.客户端代码:
package com.jym.codec;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
/**
* @program: NettyPro
* @description: Netty客户端练习
* @author: jym
* @create: 2020/02/08
*/
public class JymNettyClient {
public static void main(String[] args) throws InterruptedException {
// 客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建客户端启动对象,注意:客户端使用的不是SeverBootstrap,而是Bootstrap
Bootstrap bootstrap = new Bootstrap();
// 设置相关参数
// 设置线程组
bootstrap.group(group)
// 设置通道的实现类(反射)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//在pipeline 加入ProtoBufEncoder
pipeline.addLast(new ProtobufEncoder());
// 加入自己的处理器
pipeline.addLast(new JymNettyClientHandler());
}
});
System.out.println("客户端 is ok");
// 启动客户端去连接服务器端
// 关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();
// 给关闭通道进行监听
sync.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
ProtoBufEncoder:proto的编码器
7.客户端handler,给客户端发送消息
package com.jym.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @program: NettyPro
* @description: 处理器
* @author: jym
* @create: 2020/02/08
*/
public class JymNettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就绪就会触发该方法
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送一个student对象
StudentPOJO.Student.Builder student = StudentPOJO.Student.newBuilder().setId(4).setName("惊讶猫");
ctx.writeAndFlush(student);
}
/**
* 通道有读取时间时触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器回复的消息:"+ byteBuf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址:" + ctx.channel().remoteAddress());
}
/**
* 处理异常,关闭上下文
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
上个例子有很大的局限性,比如我们如果传的不是student类型,还需要编写其他的.proto文件,所以需要进行一个简单的处理
.proto文件,这里我们添加个worker类型,如果需要发送别的类型的数据,也可以在里添加
syntax = "proto3";
option optimize_for = SPEED; // 加快解析
option java_package = "com.jym.codec2"; //指定生成到哪个包下
option java_outer_classname = "MyDataInfo"; // 外部类名称
// protoBuf 可以使用message来管理其他message
message MyMessage {
// 定义一个枚举类型
enum DataType {
StudentType = 0; //在proto3要求enum的编号从0开始
WorkerType = 1;
}
// 用data_type 来标识传的是哪一个枚举类型
DataType data_type = 1;
// 表示每次枚举类型最多出现其中的一个,节省空间
oneof dataBody {
Student student = 2;
Worker worker = 3;
}
}
message Student {
int32 id = 1;
string name = 2;
}
message Worker {
string name = 1;
int32 age = 2;
}
服务端handler也需要改进一下
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
// 读取客户端发送的studentPojo.student
MyDataInfo.MyMessage.DataType dataType = ((MyDataInfo.MyMessage) msg).getDataType();
if(dataType == MyDataInfo.MyMessage.DataType.StudentType){
MyDataInfo.Student student = ((MyDataInfo.MyMessage) msg).getStudent();
System.out.println("学生id = "+ student.getId()+",学生name = "+student.getName());
} else if(dataType == MyDataInfo.MyMessage.DataType.WorkerType){
MyDataInfo.Worker worker = ((MyDataInfo.MyMessage) msg).getWorker();
System.out.println("工人姓名= "+ worker.getName()+",工人年龄 = "+worker.getAge());
} else {
System.out.println("传输的类型不正确");
}
}
客户端handler
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 随机发送student 或者worker
int i = new Random().nextInt(3);
MyDataInfo.MyMessage myMessage = null;
if(0 == i){
myMessage = MyDataInfo.MyMessage.newBuilder()
.setDataType(MyDataInfo.MyMessage.DataType.StudentType)
.setStudent(MyDataInfo.Student.newBuilder().setId(9527).setName("惊讶猫")
.build()).build();
} else {
myMessage = MyDataInfo.MyMessage.newBuilder()
.setDataType(MyDataInfo.MyMessage.DataType.WorkerType)
.setWorker(MyDataInfo.Worker.newBuilder().setName("惊讶猫二号").setAge(22)
.build()).build();
}
ctx.writeAndFlush(myMessage);
}