接上一篇《5.高性能nio框架netty(中)》
上一篇我们编写了使用Netty框架开发的客户端与服务端,并且详细编写了Handler处理类,又探索了InboundHandler以及OutboundHandler处理类的区别以及执行顺序。
本篇我们来总结和介绍之前两篇出现的一系列API对象:
Handler在Netty中,无疑占据着非常重要的地位。Handler与Servlet中的filter很像,通过Handler可以完成通讯报文的解码编码、拦截指定的报文、统一对日志错误进行处理、统一对请求进行计数、控制Handler执行与否。
一句话,没有它做不到的只有你想不到的。
Netty中的所有handler都实现自ChannelHandler接口。按照输入输出来分,分为ChannelInboundHandler、ChannelOutboundHandler两大类。
ChannelInboundHandler对从客户端发往服务器的报文进行处理,一般用来执行解码、读取客户端数据、进行业务处理等;
ChannelOutboundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码、发送报文到客户端。
Netty中可以注册多个handler。ChannelInboundHandler按照注册的先后顺序执行;ChannelOutboundHandler按照注册的先后顺序逆序执行。
如下图所示,按照注册的先后顺序对Handler进行排序,request进入Netty后的执行顺序为:
之前我们在样例中,传输的都是字符串类型的数据,而现实情况中,我们往往传播的是一个结构完整的封装好的Java对象。那么使用Netty如何进行对象的传输呢?实际上就是利用那个常见的发送对象----ByteBuf。
(1)ByteBuf优点
ByteBuf扮演了一种角色,就是传递数据的中介,属于一种“缓冲区”。在JDK中也有一个类似的对象,是ByteBuffer,但是ByteBuf经过一系列优化,其性能和功能比ByteBuffer好了许多。由于ByteBuf在netty是通过Channel传输的,现对于ByteBuffer,ByteBuf有以下优势:
①自定义缓冲区类型
②通过内置复合缓冲(composite Buffer)实现零拷贝
③无需使用flip()函数切换读写模式
④使用读索引和写索引(JDK只使用一个索引)
⑤引用计数
⑥Polling池
(2)ByteBuf类型
ByteBuf有heapBuffer(堆缓冲区)和directBuffer(直接缓冲区),以及composite Buffer(复合缓冲区类型)。
●heapBuffer(堆缓冲区)是将数据存储在JVM的堆空间,将数据存储在数组。但是要先将数据拷贝到直接缓冲区再进行传递。
创建语句:
//堆缓冲区
ByteBuf heapBuf = Unpooled.buffer(8);
●directBuffer(直接缓冲区)是堆之外直接分配内存,直接缓冲区不会占用堆的容量。直接缓冲池不支持数组访问数据。
创建语句:
//直接缓冲区
ByteBuf directBuf = Unpooled.directBuffer(16);
IO通信线程中读写缓冲时建议使用DirectByteBuffer,因为这涉及到大量的IO数据读写。对于后端的业务消息的编解码模块使用HeapByteBuffer,因为在JVM的堆中可以快速创建和快速释放。
●composite Buffer(复合缓冲区类型)类似于一个ByteBuf的组合视图,在这个视图里面我们可以创建不同的ByteBuf(可以是不同类型的)。这样,复合缓冲区就类似于一个列表,我们可以动态的往里面添加和删除其中的ByteBuf,JDK里面的ByteBuffer就没有这样的功能。
创建语句:
//组合缓冲区
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
//堆缓冲区
ByteBuf heapBuf = Unpooled.buffer(8);
//直接缓冲区
ByteBuf directBuf = Unpooled.directBuffer(16);
//添加不同类型的ByteBuf到CompositeByteBuf
compBuf.addComponents(heapBuf, directBuf);
(3)ByteBuf工作原理
ByteBuf中将消息分为两部分,一条消息由Header和Body组成,也就是消息头和消息尾:
消息头中写入要发送数据的总长度,通常是在消息头的第一个字段使用int值来标识发送数据的长度。消息体中写入要发送的数据的byte字节码数组。
注:ByteBuf里面的数据都是保存在字节数组里面的。
AbstractByteBuf抽象类基本实现了ByteBuf,在AbstractByteBuf里面定义了下面5个变量:
//源码
int readerIndex; //读索引
int writerIndex; //写索引
private int markedReaderIndex;//标记读索引
private int markedWriterIndex;//标记写索引
private int maxCapacity;//缓冲区的最大容量
ByteBuf提供了两个索引指针变量来支持读写操作,读操作使用的是readerIndex(),写操作使用的是writerIndex()
ByteBuf一定满足的是:0<=readerIndex<=writerIndex<=capacity
下图显示了一个ByteBuf中可以被划分为三个区域:
其中discardable bytes为可回收字节,即对于已经读过的字节,需要回收。通过调用ByteBuf.discardReadBytes()来回收已经读取过的字节,discardReadBytes()将回收从索引0到readerIndex之间的字节,将readable bytes区域移到开始位置,将discardable bytes可回收字节扩充到writerable bytes区域:
整个ByteBuf区域的读取区域划分模式(图片摘自李林峰老师《Netty权威指南》):
当我们读取字节的时候,一般要先判断buffer中是否有字节可读,这时候可以调用isReadable()函数来判断:源码如下:
@Override
public boolean isReadable() {
return writerIndex > readerIndex;
}
其实也就是判断 读索引是否小于写索引 来判断是否还可以读取字节。在判断是否可写时也是判断写索引是否小于最大容量来判断。
@Override
public boolean isWritable() {
return capacity() > writerIndex;
}
清除ByteBuf来说,有两种形式,第一种是clear()函数:源码如下:
@Override
public ByteBuf clear() {
readerIndex = writerIndex = 0;
return this;
}
很明显这种方式并没有真实的清除缓冲区中的数据,而只是把读/写索引值重新都置为0了,这与discardReadBytes()方法有很大的区别。
从源码可知,每个ByteBuf有两个标注索引,
private int markedReaderIndex;//标记读索引
private int markedWriterIndex;//标记写索引
可以通过markReaderIndex()、markWriterIndex()标记方法,标记一下当前的读、写索引的位置
可以通过重置(resetReaderIndex、resetWriterIndex)方法返回上次标记的索引的位置。
Netty中,通讯的双方建立连接后,会把数据按照ByteBuf的方式进行传输。例如http协议中,就是通过HttpRequestDecoder对ByteBuf数据流进行处理,转换成http的对象。基于这个思路,可以自定义一种协议,Server和客户端直接传输java对象。
实现的原理是通过Ecoder把java对象转换成ByteBuf流进行传播,通过Decoder把ByteBuf转换成java对象进行处理,出逻辑如下图所示:
其中XxxxEncoder就是针对Xxxx这种Java对象进行解码转换,转换成ByteBuf流传播。然后在服务端,使用XxxxDecoder将ByteBuf转换成Xxxx类型的java对象。
例如一个Person的对象,在从Netty传输过来后,是如何转换的:
上图就是一个转换的详细过程,首先Netty拿到的是发送对象ByteBuf,然后发送对象ByteBuf被转换为byte数组,然后byte数组进行反序列化,如果是String就直接使用String类进行包装生成String,如果是对象(例如Person对象),此时将byte数组包装进ArrayInputStream数组流,然后将ArrayInputStream数组流包装进ObjectInputStream对象流中,最终将Object对象转换为Person类。
下面我们来编写发送User对象的一个样例。下图是整个样例的结构:
其中User是需要网络传输的Java对象;然后是客户端的启动类“NettyTestClient”以及业务处理类“NettyTestClientHandler”、然后是服务端的启动类“NettyTestServer”以及业务处理类“NettyTestServerHandler”、User对象的序列化和反序列化类“UserEncoder”、“UserDecoder”,最后是byte数组与ByteBuf转换工具类“ByteBufBytesConverter”、byte数组与Object转换工具类“ByteObjConverter”。首先编写一个User的Bean对象:
package cn.com.netty.bytebuf.test.bean;
public class User implements Serializable{
private String name;
private String age;
private String gender;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
这里要注意,需要进行网络传输的User对象,一定要实现Serializable接口,才可以进行序列化操作。
之前我们在客户端发送数据和服务端接受数据都是先拿到ByteBuf转换成Byte,服务端写出时又将byte转换成Bytebuf。我们这里将这一块封装成一个转换器:
package cn.com.netty.bytebuf.test.utils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class ByteBufBytesConverter {
public byte[] read(ByteBuf in) {
//读取数据
byte[] req = new byte[in.readableBytes()];
in.readBytes(req);
return req;
}
public ByteBuf write(byte[] bytes) {
//写出数据
ByteBuf resp = Unpooled.copiedBuffer(bytes);
return resp;
}
}
然后拿到的Byte数组还需要转换成相应的User对象,或者接收的User对象要转换为Byte数组,所以这里我们再编写一个Byte和实体类相互转换的转换器:
package cn.com.netty.bytebuf.test.utils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ByteObjConverter {
/**
* 使用IO的outputstream流将object转换为byte[]
* @param Object
* @return byte[]
* */
public static byte[] objectToByte(Object obj){
byte[] bytes = null;
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = null;
try {
oo = new ObjectOutputStream(bo);
oo.writeObject(obj);
bytes = bo.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
bo.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
oo.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return bytes;
}
/**
* 使用IO的iutputstream流将byte[]转换为object
* @param byte[]
* @return Object
* */
public static Object byteToObject(byte[] bytes) {
Object obj = null;
ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
ObjectInputStream oi = null;
try {
oi = new ObjectInputStream(bi);
obj = oi.readObject();
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
bi.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
oi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return obj;
}
}
这里就是使用ByteArrayOutputStream、ByteArrayInputStream来封装Byte数组输出、输入的流、ObjectOutputStream、ObjectInputStream来封装Object对象的写入写出流,用以Byte数组和Object对象之间的相互转换。
然后就是User的序列化和反序列化的处理类:
package cn.com.netty.bytebuf.test.coder;
import cn.com.netty.bytebuf.test.bean.User;
import cn.com.netty.bytebuf.test.utils.ByteObjConverter;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* 序列化
* 将Object转换成byte[]
* */
public class UserEncoder extends MessageToByteEncoder{
@Override
protected void encode(ChannelHandlerContext ctx, User msg, ByteBuf out) throws Exception {
//工具类,将object转换为byte[]
byte[] datas = ByteObjConverter.objectToByte(msg);
out.writeBytes(datas);
ctx.flush();
}
}
package cn.com.netty.bytebuf.test.coder;
import java.util.List;
import cn.com.netty.bytebuf.test.utils.ByteBufBytesConverter;
import cn.com.netty.bytebuf.test.utils.ByteObjConverter;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* 反序列化
* 将Byte[]转换为Object
* */
public class UserDecoder extends ByteToMessageDecoder{
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
上面两个类都是借助ByteObjConverter工具类进行Object和byte的相互转换。然后就是客户端的启动类“NettyTestClient”:
package cn.com.netty.bytebuf.test.client;
import java.net.InetSocketAddress;
import cn.com.netty.bytebuf.test.coder.UserEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyTestClient {
private final String host;//服务端连接ip
private final int port;//服务端连接端口
public NettyTestClient(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws Exception {
new NettyTestClient("localhost",8888).start();
}
public void start() throws Exception{
EventLoopGroup nioEventLoopGroup = null;
try {
Bootstrap bootstrap = new Bootstrap();//客户端引导类
//EventLoopGroup可以理解为是一个线程池,这个线程池用来处理连接、接受数据、发送数据
nioEventLoopGroup = new NioEventLoopGroup();
bootstrap.group(nioEventLoopGroup)//多线程处理
.channel(NioSocketChannel.class)//制定通道类型为NioSocketChannel
.remoteAddress(new InetSocketAddress(host,port))//地址
.handler(new ChannelInitializer(){//业务处理类
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//注册编码的Handler
ch.pipeline().addLast(new UserEncoder());
//注册处理消息的Handler
ch.pipeline().addLast(new NettyTestClientHandler());
}
});
//连接服务器
ChannelFuture channelFuture = bootstrap.connect().sync();
channelFuture.channel().closeFuture().sync();
}finally{
nioEventLoopGroup.shutdownGracefully().sync();
}
}
}
这里其它代码都是之前写的模板代码,除了给pipeline设置Handler处理器类稍有不同,因为我们的业务处理类Handler处理完之后,需要发送一个User对象出去,此时就需要将Encoder序列化,即将Object转换成byte[]传输出去。所以这里我们注册了编码的Handler----UserHandler,用来序列化User,将接收到的byte转换为User对象。然后是业务处理Handler,这里之所以将UserEncoder放在NettyTestClientHandler,是因为UserEncoder是一个OutHandler,前面提到过,OutHandler不能放在最后。
然后是客户端业务处理类“NettyTestClientHandler”:
package cn.com.netty.bytebuf.test.client;
import cn.com.netty.bytebuf.test.bean.User;
import cn.com.netty.bytebuf.test.utils.ByteBufBytesConverter;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyTestClientHandler extends SimpleChannelInboundHandler{
//客户端连接服务器后被调用
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client 开始发送数据..");
User user = new User();
user.setName("jack");
user.setAge("26");
user.setGender("boy");
user.setAddress("china");
System.out.println("client 发送内容:"+user.toString());
ctx.write(user);
ctx.flush();
}
//从服务器收到数据后调用
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
System.out.println("client 读取server数据..");
//服务器返回消息后
ByteBufBytesConverter byteBufBytesConverter = new ByteBufBytesConverter();
byte[] req = byteBufBytesConverter.read(buf);//创建一个存储信息的byte数组
buf.readBytes(req);//将buffer中的数据读到byte数组中
String body = new String(req,"UTF-8");//将byte数组转换为String(并转码)
System.out.println("服务端数据为:"+body);//打印服务端反馈的信息
}
//发生异常时调用
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("client exceptionCaught...");
//释放资源
ctx.close();
}
}
这里就是在获取连接后,创建一个User对象,将该对象写出去。该Handler写出去之后,就会到之前的UserHandler中进行序列化处理。
服务端的启动类“NettyTestServer”:
package cn.com.netty.bytebuf.test.server;
import cn.com.netty.bytebuf.test.coder.UserDecoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyTestServer {
private final int port;
public NettyTestServer(int port) {
this.port = port;
}
public void start() throws Exception{
EventLoopGroup eventLoopGroup = null;
try {
//server端引导类,来引导绑定和启动服务器;
ServerBootstrap serverBootstrap = new ServerBootstrap();
//连接池处理数据
eventLoopGroup = new NioEventLoopGroup();
//装配ServerBootstrap
serverBootstrap.group(eventLoopGroup)//多线程处理
//制定通道类型为NioServerSocketChannel,一种异步模式的可以监听新进来的TCP连接的通道
.channel(NioServerSocketChannel.class)
.localAddress("localhost",port)//设置InetSocketAddress让服务器监听某个端口以等待客户端连接。
.childHandler(new ChannelInitializer(){//设置childHandler执行所有的连接请求
@Override
protected void initChannel(Channel ch) throws Exception {
//注册解码的Handler
ch.pipeline().addLast(new UserDecoder());
//注册业务处理Handler
ch.pipeline().addLast(new NettyTestServerHandler());
}
});
//最后绑定服务器等待直到绑定完成,调用sync()方法会阻塞直到服务器完成绑定,然后服务器等待通道关闭
//因为使用sync方法,所以关闭操作也会被阻塞。
ChannelFuture channelFuture = serverBootstrap.bind().sync();
System.out.println("开始监听,端口为:"+channelFuture.channel().localAddress());
channelFuture.channel().closeFuture().sync();
}finally{
eventLoopGroup.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new NettyTestServer(8888).start();
}
}
在启动类中,其它代码和之前的模板代码没有区别,不同的是pipeline首先注册了解码的Handler,然后注册业务处理类Handler;这里的UsderDecoder是一个InHandler,按照顺序执行的方式,这里会先执行该Handler进行反序列化,将User对象转换为byte数组,然后再去执行业务处理Handler。
在服务端业务处理类“NettyTestServerHandler”中:
package cn.com.netty.bytebuf.test.server;
import java.util.Date;
import cn.com.netty.bytebuf.test.bean.User;
import cn.com.netty.bytebuf.test.utils.ByteBufBytesConverter;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class NettyTestServerHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server 读取数据......");
//读取数据
User user = (User)msg;
System.out.println(user.getName());
System.out.println(user.getAge());
System.out.println(user.getGender());
System.out.println(user.getAddress());
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("server 读取数据完毕...");
ctx.flush();//刷新后才将数据发出到SocketChannel
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
这里因为使用了UsderDecoder先进行了解码,所以接受到的Object可以直接转换为User对象了。
下面我们首先启动服务端启动类:
等待客户端连接。然后启动客户端启动类:
此时可以看到,客户端成功写出User对象,而服务端也成功接收到了User对象:
至此,我们编写了使用EncoderHandler和DecoderHandler实现了一个Java对象的写入写出的样例。
有关Netty的相关知识到此为止,想实现自定义rpc框架,请继续关注本系列后面的文章。
参考:
https://www.cnblogs.com/yaboya/p/9176703.html
https://blog.csdn.net/u010853261/article/details/53690780
https://blog.csdn.net/woshixuye/article/details/54286773
传智播客《2017零基础大数据》教学视频
转载请注明出处:https://blog.csdn.net/acmman/article/details/88074717