【RPC高性能框架总结】6.高性能nio框架netty(下)

接上一篇《5.高性能nio框架netty(中)》
上一篇我们编写了使用Netty框架开发的客户端与服务端,并且详细编写了Handler处理类,又探索了InboundHandler以及OutboundHandler处理类的区别以及执行顺序。
本篇我们来总结和介绍之前两篇出现的一系列API对象:

1.Netty中的Handler业务处理类

Handler在Netty中,无疑占据着非常重要的地位。Handler与Servlet中的filter很像,通过Handler可以完成通讯报文的解码编码、拦截指定的报文、统一对日志错误进行处理、统一对请求进行计数、控制Handler执行与否。
一句话,没有它做不到的只有你想不到的。

Netty中的所有handler都实现自ChannelHandler接口。按照输入输出来分,分为ChannelInboundHandler、ChannelOutboundHandler两大类。
ChannelInboundHandler对从客户端发往服务器的报文进行处理,一般用来执行解码、读取客户端数据、进行业务处理等;
ChannelOutboundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码、发送报文到客户端。

Netty中可以注册多个handler。ChannelInboundHandler按照注册的先后顺序执行;ChannelOutboundHandler按照注册的先后顺序逆序执行。
如下图所示,按照注册的先后顺序对Handler进行排序,request进入Netty后的执行顺序为:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第1张图片


2.Netty发送对象ByteBuf

之前我们在样例中,传输的都是字符串类型的数据,而现实情况中,我们往往传播的是一个结构完整的封装好的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组成,也就是消息头和消息尾:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第2张图片
消息头中写入要发送数据的总长度,通常是在消息头的第一个字段使用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中可以被划分为三个区域: 
【RPC高性能框架总结】6.高性能nio框架netty(下)_第3张图片
其中discardable bytes为可回收字节,即对于已经读过的字节,需要回收。通过调用ByteBuf.discardReadBytes()来回收已经读取过的字节,discardReadBytes()将回收从索引0到readerIndex之间的字节,将readable bytes区域移到开始位置,将discardable bytes可回收字节扩充到writerable bytes区域:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第4张图片
整个ByteBuf区域的读取区域划分模式(图片摘自李林峰老师《Netty权威指南》):
【RPC高性能框架总结】6.高性能nio框架netty(下)_第5张图片

当我们读取字节的时候,一般要先判断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)方法返回上次标记的索引的位置。

3.使用Netty传送Java对象

Netty中,通讯的双方建立连接后,会把数据按照ByteBuf的方式进行传输。例如http协议中,就是通过HttpRequestDecoder对ByteBuf数据流进行处理,转换成http的对象。基于这个思路,可以自定义一种协议,Server和客户端直接传输java对象。

实现的原理是通过Ecoder把java对象转换成ByteBuf流进行传播,通过Decoder把ByteBuf转换成java对象进行处理,出逻辑如下图所示:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第6张图片
其中XxxxEncoder就是针对Xxxx这种Java对象进行解码转换,转换成ByteBuf流传播。然后在服务端,使用XxxxDecoder将ByteBuf转换成Xxxx类型的java对象。
例如一个Person的对象,在从Netty传输过来后,是如何转换的:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第7张图片
上图就是一个转换的详细过程,首先Netty拿到的是发送对象ByteBuf,然后发送对象ByteBuf被转换为byte数组,然后byte数组进行反序列化,如果是String就直接使用String类进行包装生成String,如果是对象(例如Person对象),此时将byte数组包装进ArrayInputStream数组流,然后将ArrayInputStream数组流包装进ObjectInputStream对象流中,最终将Object对象转换为Person类。

下面我们来编写发送User对象的一个样例。下图是整个样例的结构:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第8张图片
其中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 out) throws Exception {
        //工具类,将ByteBuf转换为byte[]
        ByteBufBytesConverter byteBufBytesConverter = new ByteBufBytesConverter();
        byte[] bytes = byteBufBytesConverter.read(in);
        //工具类,将byte[]转换为Object
        Object obj = ByteObjConverter.byteToObject(bytes);
        out.add(obj);
    }
    
} 
  

上面两个类都是借助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对象了。

下面我们首先启动服务端启动类:

等待客户端连接。然后启动客户端启动类:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第9张图片
此时可以看到,客户端成功写出User对象,而服务端也成功接收到了User对象:
【RPC高性能框架总结】6.高性能nio框架netty(下)_第10张图片
至此,我们编写了使用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

你可能感兴趣的:(Netty,RPC,手写RPC框架)