Netty(一)Buffer,Channel,Selector

前言

在正式学习Netty之前,还是得先学习一下基础的组件,Java NIO相比Java BIO有了较大的变化,这种变化也是面试主要问到的地方。

Buffer

buffer其实英译过来就是缓冲的意思,我们可以直接理解为其实就是内存中的一块,可以将数据写入buffer,然后从buffer中读取数据。在Java中定义了不同的buffer,如下图所示(原博客:NIO的三大组件):

 我们用的较多的也就是ByteBuffer,其他的buffer其实也就是对ByteBuffer进行了不同程度的包装,从某一种程度上来说,通过源码我们发现其实buffer的数据结构就是一个byte数组,同时在父类Buffer中维护了三个主要的属性,如下所示:

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

position,limit,capacity这三属性配合完成了buffer的读写。 同时源码中也说明了这三者的大小关系,至于mark这个后面会讨论。其中capacity就是定义的buffer的容量大小,position的初始值是0 ,这个表示下一次写入元素的位置。limit:代表写操作模式下最大能写入的数据,读模式下,limit又等于buffer中的实际数据个数。

初始化

通过源码可以看出,初始化buffer有三个方法

/**
    使用最多的分配方式,直接分配capacity的HeapByteBuffer
*/
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

/**
    返回一个DirectByteBuffer
*/
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

/**
    通过一个byte数组分配buffer,返回HeapByteBuffer
*/
public static ByteBuffer wrap(byte[] array,
                                int offset, int length){
    try {
        return new HeapByteBuffer(array, offset, length);
    } catch (IllegalArgumentException x) {
        throw new IndexOutOfBoundsException();
    }
}

可以看出,allocateDirect返回一个DirectByteBuffer,另外两个方法返回HeapByteBuffer,这两者的区别如下:

HeapByteBuffer:顾名思义,其实是建立在JVM堆上的buffer。

DirectByteBuffer:底层的数据其实是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向了数据,从而操作数据。

前者把内容写进buffer里速度会快些;并且,可以更容易回收。后者跟外设(IO设备)打交道时会快很多,因为外设读取jvm堆里的数据时,不是直接读取的,而是把jvm里的数据读到一个内存块里,再在这个块里读取的,如果使用DirectByteBuffer,则可以省去这一步,实现zero copy(零拷贝)

初始化一个capacity为11后的buffer结构如下所示:

将数据放入buffer

/**
    将单个元素放入到buffer中
*/
public abstract ByteBuffer put(byte b);
public abstract ByteBuffer put(int index, byte b);

/**
    将数组或buffer放入到buffer中
*/
public final ByteBuffer put(byte[] src)
public ByteBuffer put(byte[] src, int offset, int length)
public ByteBuffer put(ByteBuffer src)

这里面的源码都比较简单,但是我们常用的是

channel.read(buffer) 

实例:如果放入4个元素之后,buffer中的byte数组示意图如下:

这个时候只是position地址有了变化,position永远指向下一个待放入元素的索引。

读取buffer中的元素

如果需要读取buffer中的元素,需要切换buffer的模式,在读取数据之前,需要调用buffer的flip的方法,这个方法的源码如下:

public final Buffer flip() {
    limit = position;//将limit设置为实际写入的数据数量
    position = 0;    //position归零
    mark = -1;    //mark就是一个标记
    return this;
}

可理解为,将读取区域锁定,flip方法之后,buffer的示意图如下

利用channel读取数据到buffer中的方法如下:

channel.write(buffer);

 buffer中提供的get方式

public abstract ByteBuffer put(byte b);
public abstract byte get(int index);

public ByteBuffer get(byte[] dst, int offset, int length)
public ByteBuffer get(byte[] dst)

这几个参数和put方式差不多。 

mark和reset方法

这两个方法是用于辅助读取的方式,mark其实用于临时保存position的值,每次调用mark方法,就会将mark设置为当前的position的值

public final Buffer mark() {
    mark = position;
    return this;
}

 如果position为5,这个时候调用mark(),之后我们开始读取元素,读取到10的时候,如果突然需要再从5读取到10,重新读取一遍,这个时候我们不用强制将position置为5,只需要调用reset即可,源码如下所示:

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

buffer的重置

rewind方法

这个方法只是会简单重置position为0,从头开始读取buffer。源码如下:

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

clear方法

将limit,position和mark复原,但是没有清空buffer中的数据

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

compact方法

 这个方法会在重置相关索引的时候,同时清空buffer。

public ByteBuffer compact() {
	System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());
    discardMark();
    return this;
}

实例

下面以FileChannel为基础,实现了一个buffer的简单实例

/**
 * autor:liman
 * createtime:2019/10/6
 * comment:
 */
public class ByteBufferDemo {

    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("E:/liman/learn/test.txt");
        //创建文件的操作channel
        FileChannel channel = fileInputStream.getChannel();

        //分配一个长度为10的buffer
        ByteBuffer buffer = ByteBuffer.allocate(10);
        printBuffer("初始化",buffer);

        channel.read(buffer);

        //操作之前先调用flip,锁定读取buffer的范围
        buffer.flip();
        printBuffer("锁定读取数据的范围",buffer);

        //判断有没有可读数据
        System.out.println(buffer.remaining());
        while(buffer.remaining()>=0){
            byte b = buffer.get();
            System.out.print((char)b);
            channel.write(buffer);
        }
        printBuffer("获取buffer中的数据",buffer);

        buffer.clear();

        fileInputStream.close();
    }

    /**
     * 打印buffer的一些状态
     * @param step
     * @param buffer
     */
    private static void printBuffer(String step,ByteBuffer buffer){
        System.out.println(step+":");
        //容量,数组大小
        System.out.print("capacity: " + buffer.capacity() + ", ");
        //当前操作数据所在的位置,也可以叫做游标
        System.out.print("position: " + buffer.position() + ", ");
        //锁定值,flip,数据操作范围索引只能在position - limit 之间
        System.out.println("limit: " + buffer.limit());
        System.out.println();
    }

}

Channel

所有的NIO操作,其实都是对channel的操作。常用的Channel有四种,FileChannel,SocketChannel,DatagramChannel,ServerSocketChannel。这四者的主要区别如下:

FileChannel:文件通道,用于文件的读和写
DatagramChannel:用于 UDP 连接的接收和发送
SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求

上述实例中已经利用FileChannel举了一个ByteBuffer的实例,但是需要注意的是FileChannel是不支持非阻塞的。这里重点介绍SocketChannel和ServerSocketChannel

SocketChannel

客户端的SocketChannel创建过程如下:

InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080);
SocketChannel socketChannel = SocketChannel.open();//客户端开启一个channel,在客户端实例化
socketChannel.connect(inetSocketAddress);    //channel与服务端建立连接

ServerSocketChannel

ServerSocketChannel的创建过程如下:

ServerSocketChannel channel = ServerSocketChannel.open();

channel.configureBlocking(false);//将ServerSocketChannel设置为非阻塞,默认情况下是阻塞

channel.socket().bind(new InetSocketAddress(8080)//服务端监听8080端口

//获取客户端的SocketChannel,这里也是SocketChannel的第二个实例化方法,在服务端实例化
SocketChannel socketChannel = serverSocketChannel.accept();

到这里应该能看出SocketChannel是一个双关的网络channel,支持可读写。同时ServerSocketChannel不直接和buffer打交道,与buffer打交道的是真正的SocketChannel,ServerSocketChannel只负责监听,如果发现客户端有请求,则为这个请求创建一个SocketChannel之后ServerSocketChannel的任务就完成了。

Selector

Selector其实我个人理解为Channel的管理者或调度者,但是Selector只是管理非阻塞的channel

先介绍一下Selector的基础操作,后面总结NIO和BIO的时候再仔细介绍selector

创建Selector

Selector selector = Selector.open();

与channel的关系

如果channel交给Selector管理,就必须要将channel注册到Selector上

将channel注册到Selector上,具体的方法如下:

第一个参数就是Selector的实例,第二个参数是事件类型。返回一个SelectionKey,这个就好比一个注册之后的唯一用户名。

public final SelectionKey register(Selector sel, int ops)
    throws ClosedChannelException{
    return register(sel, ops, null);
}

针对第二个参数的定义,源码如下:

//通道中有数据可以进行读取
public static final int OP_READ = 1 << 0;

//可以往通道中写入数据
public static final int OP_WRITE = 1 << 2;

//成功建立 TCP 连接
public static final int OP_CONNECT = 1 << 3;

//接受 TCP 连接
public static final int OP_ACCEPT = 1 << 4;

 如果需要监听多个事件,则只需要传入两个事件的异或操作的结果即可。

注册之后,该方法返回SelectionKey,这个SelectionKey包含了Channel和Selector的信息,可以简单理解为Channel注册到Selector之后的唯一标识

Selector处理注册的事件

主要通过select方法判断是否有事件准备好,如果已经有事件准备好了,则获取SelectorKeys的集合,处理每一个key的数据。一段简单的伪码表示Selector对事件的处理

// 判断是否有事件准备好
int readyChannels = selector.select();
if(readyChannels == 0) continue;

// 遍历
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
  SelectionKey key = keyIterator.next();

  if(key.isAcceptable()) {
      // a connection was accepted by a ServerSocketChannel.

  } else if (key.isConnectable()) {
      // a connection was established with a remote server.

  } else if (key.isReadable()) {
      // a channel is ready for reading

  } else if (key.isWritable()) {
      // a channel is ready for writing
  }

  keyIterator.remove();
}

总结

本文简单总结了Java NIO的三大组件,参考了大牛的博客,自己只是写入了一些实例,如果觉得本人博客写的较为晦涩的,可以直接移步大牛的博客:传送门——Java NIO 三大组件

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/102246870