- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码。
示意图:
codec
(编解码器) 的组成部分有两个:decoder
(解码器)和encoder
(编码器)。encoder
负责把业务数据转换成字节码数据,decoder
负责把字节码数据转换成业务数据
Netty 本身的编码解码的机制和问题分析
Netty 自身提供了一些 codec(编解码器)
- Netty 提供的编码器
StringEncoder,对字符串数据进行编码
ObjectEncoder,对 Java 对象进行编码
...
- Netty 提供的解码器
StringDecoder, 对字符串数据进行解码
ObjectDecoder,对 Java 对象进行解码
...
- Netty 本身自带的 ObjectDecoder 和 ObjectEncoder 可以用来实现 POJO对象或各种业务对象的编码和解码,底层使用的仍是 Java 序列化技术 , 而Java 序列化技术本身效率就不高,存在如下问题
无法跨语言,如果服务端是java写的,客户端也要求是java写的
序列化后的体积太大,是二进制编码的 5 倍多。
序列化性能太低
=> 引出 新的解决方案 [Google 的 Protobuf]
Protobuf
- Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC[远程过程调用 remote procedure call ] 数据交换格式 。目前很多公司从使用
http+json
转换成tcp+protobuf
来进行数据交换 - 参考文档 :
https://developers.google.com/protocol-buffers/docs/proto
语言指南 - Protobuf 是以 message 的方式来管理数据的.
- 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的] (支持目前绝大多数语言,例如 C++、C#、Java、python 等)
- 高性能,高可靠性
- 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用.proto 文件进行描述。说明,在idea 中编写.proto 文件时,会自动提示是否下载 .ptotot 编写插件. 可以让语法高亮。
- 然后通过 protoc.exe 编译器根据.proto 自动生成.java 文件
- protobuf 使用示意图
Protobuf快速入门实例1
编写程序,使用Protobuf完成如下功能:
- 客户端可以发送一个Student PoJo 对象到服务器 (通过 Protobuf 编码)
- 服务端能接收Student PoJo 对象,并显示信息(通过 Protobuf 解码)
生成xx.java类
引入依赖:
<!--protobuf-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.6.1</version>
</dependency>
编写Student.proto
:在创建文件Student.proto
的时候,idea
会提示安装protobuf
插件,安装就完事了。
syntax="proto3"; //版本
option java_outer_classname="StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf使用message管理数据
message Student{//会在StudentPOJO外部类生成一个内部类Student,它是真正发送的pojo对象
int32 id =1;//student类中有一个属性名字为id 类型为int32(protobuf类型)对应java中的int;1表示属性序号,不是值
string name=2;
}
编译Student.proto
:
获取编译器protoc-3.6.1-win32.zip
:
链接:https://pan.baidu.com/s/18PIMuCVnwXYyqExlOvkmXg
提取码:cgd2
解压后,bin
目录下,把Student.proto
放入,cmd
进入bin
目录;执行命令protoc.exe --java_out=. Student.proto
生成了对应的Java
类:
ProtoBuf实例使用
服务端核心代码:
//创建一个通道测试对象(匿名对象)
new ChannelInitializer<SocketChannel>() {
//给管道设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//在管道中加入protobuf解码器,指定对哪种对象进行解码
pipeline.addLast("decoder",new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
pipeline.addLast(new NettyServerHandler());
}
}
服务端自定义handler:
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数据事件(可以读取客户端发送的消息)
* 1.ChannelHandlerContext:上下文对象,含有管道pipeline,通道channel,地址。
* 管道和通道区别:管道里面是处理器(处理数据),通道里面是buffer写入的数据(传输数据)
*2.msg: 客户端发送的数据
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//读取从客户端发送的StudentPOJO.Student
StudentPOJO.Student student = (StudentPOJO.Student) msg;
System.out.println("客户端发送的数据:id="+student.getId()+" name="+student.getName());
}
}
客户端核心代码:
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//加入ProtobufEncoder编码器
pipeline.addLast("encoder",new ProtobufEncoder());
//加入自己的处理器
pipeline.addLast(new NettyClientHandler());
}
}
客户端自定义handler:
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//发送一个student对象到服务器
StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("张三").build();
ctx.writeAndFlush(student);
}
}
目录结构:
测试:启动客户端,启动服务端。
Protobuf快速入门实例2
编写程序,使用Protobuf完成如下功能
- 客户端可以随机发送Student PoJo/ Worker PoJo 对象到服务器 (通过 Protobuf 编码)
- 服务端能接收Student PoJo/ Worker PoJo 对象(需要判断是哪种类型),并显示信息(通过 Protobuf 解码)
生成xx.java类
编写Student.proto
并编译:
syntax="proto3"; //版本
option optimize_for=SPEED;//加快解析
option java_package="com.example.niodemo.netty.codec2";//指定生成到哪个包下
option java_outer_classname="MyDataInfo";//外部类名称,文件名
//protobuf可以使用message管理其他message
message MyMessage{
//定义一个枚举类型
enum DataType{
studentType=0;//在proto3中,要求enum编号从0开始
workerType=1;
}
//用data_type来标识传的是哪个枚举类型,1表示属性序号
DataType data_type=1;
//标识每次枚举类型最多只能出现其中的一个,节省空间
oneof dataBody{
Student student=2;
Worker worker=3;
}
}
//protobuf使用message管理数据
message Student{//会在StudentPOJO外部类生成一个内部类Student,它是真正发送的pojo对象
int32 id =1;//student类中有一个属性名字为id 类型为int32(protobuf类型)对应java中的int;1表示属性序号,不是值
string name=2;
}
message Worker{
string name=1;
int32 age=2;
}
ProtoBuf代码
服务端完整代码:
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//设置main方法日志级别
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
List<Logger> loggerList = loggerContext.getLoggerList();
loggerList.forEach(logger -> {
logger.setLevel(Level.WARN);
});
//1.创建BossGroup 和 WorkerGroup
//说明:
//创建两个线程组 bossGroup和workerGroup
//bossGroup只是处理连接请求
//workerGroup真正的和客户端进行业务处理
//两个都是无限循环
//默认bossGroup和workerGroup含有的子线程(NioEventLoop)的个数=2*CPU核数
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(3);
try {
//2.创建服务器端的启动对象,配置参数
ServerBootstrap serverBootstrap = new ServerBootstrap();
//3.使用链式编程进行设置
serverBootstrap
//设置两个线程组
.group(bossGroup,workerGroup)
//使用NioServerSocketChannel作为服务器的通道实现
.channel(NioServerSocketChannel.class)
//当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。
// 如果未设置或所设置的值小于1,Java将使用默认值50。
.option(ChannelOption.SO_BACKLOG,128)
//设置保持活动连接状态, 是否启用心跳保活机制
.childOption(ChannelOption.SO_KEEPALIVE,true)
//给我们的workerGroup的EventLoopGroup对应的管道设置处理器Handler
.childHandler(
//创建一个通道测试对象(匿名对象)
new ChannelInitializer<SocketChannel>() {
//给管道设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//ProtoBuf核心代码start-------------------------
//在管道中加入protobuf解码器,指定对哪种对象进行解码
pipeline.addLast("decoder",new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
pipeline.addLast(new NettyServerHandler());
//ProtoBuf核心代码end-------------------------
}
});
System.out.println("...服务器 is ready ...");
//4.绑定一个端口并且同步,生成一个ChannelFuture对象
//启动服务器并绑定端口
ChannelFuture sync = serverBootstrap.bind(6668).sync();
sync.addListener((ChannelFutureListener) channelFuture -> {
if (channelFuture.isSuccess()){
System.out.println("绑定完成");
}else {
System.out.println("绑定失败");
}
});
//5.对关闭通道进行监听
sync.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务端handler
:
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
MyDataInfo.MyMessage message = (MyDataInfo.MyMessage) msg;
//根据DataType来显示不同的信息
MyDataInfo.MyMessage.DataType dataType = message.getDataType();
if (dataType==MyDataInfo.MyMessage.DataType.studentType){
MyDataInfo.Student student = message.getStudent();
System.out.println("student:id="+student.getId()+" name="+student.getName());
}else if (dataType==MyDataInfo.MyMessage.DataType.workerType){
MyDataInfo.Worker worker = message.getWorker();
System.out.println("worker:id="+worker.getAge()+" name="+worker.getName());
}else {
System.out.println("传输了类型不正确");
}
}
}
客户端:
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//设置main方法日志级别
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
List<Logger> loggerList = loggerContext.getLoggerList();
loggerList.forEach(logger -> {
logger.setLevel(Level.WARN);
});
//客户端需要一个事件循环组
EventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是serverBootStrap而是BootStrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap
//设置线程组
.group(eventExecutors)
//客户端通道的实现类(反射)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//核心代码start-----------------------------------
//加入ProtobufEncoder编码器
pipeline.addLast("encoder",new ProtobufEncoder());
//加入自己的处理器
pipeline.addLast(new NettyClientHandler());
//核心代码end-----------------------------------
}
});
System.out.println("客户端...ok...");
//启动客户端去连接服务端
//关于ChannelFuture后面会分析,设计netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} finally {
eventExecutors.shutdownGracefully();
}
}
}
客户端handler
:
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//随机的发送Student或worker对象
int random=new Random().nextInt(3);
MyDataInfo.MyMessage message=null;
if (0==random){//发送一个student对象
message = MyDataInfo.MyMessage.newBuilder().setDataType(
MyDataInfo.MyMessage.DataType.studentType)
.setStudent(MyDataInfo.Student.newBuilder().setId(5).setName("李四").build()).build();
}else {//发送一个worker对象
message = MyDataInfo.MyMessage.newBuilder().setDataType(
MyDataInfo.MyMessage.DataType.workerType)
.setWorker(MyDataInfo.Worker.newBuilder().setAge(20).setName("老王").build()).build();
}
ctx.writeAndFlush(message);
}
}
测试:启动服务端,客户端多启动几个。