Java NIO

NIO与OIO的对比

1.OIO事面向流的,NIO是面向缓冲区的。OIO是面向字节流或字符流的,在一般的OIO操作中,一流式的方法顺序地从一个流中读取一个或多个字节,因此,不能随意地改变读取指针的位置。NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。读取和写入,只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到Channel中。可以随意地读取Buffer中任意位置的数据。
2.OIO的操作是阻塞的,而NIO的操作是非阻塞的。
3.OIO没有选择器的概念,而NIO有选择器的概念。

Buffer缓冲区

应用程序与Channel主要的交互操作,就是进行数据的read读取和write写入。通道的读取就是将数据从通道读取到缓冲区;通道的写入就是将数据从缓冲区写入到通道中。

NIO的Buffer(缓冲区)本质是一个内存块,既可以写入数据,也可以从中读取数据。NIO的Buffer类,是一个抽象类,位于java.nio包中,其内部类是一个内存块(数组)。

Buffer类是一个非线程安全类。

Buffer类是一个抽象类,对应与java的主要数据类型,在NIO中有8种缓冲区类型。分别是:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer,MappedByteBuffer.

前7种Buffer类型,覆盖了能在IO中传输的所有java基本数据类型。第8种类型MappedByteBuffer是专门用于内存映射的一种ByteBuffer.

Buffer类属性

Buffer类在其内部有一个byte[]数组内存块,作为内存缓冲区。为了记录读写的状态和位置,提供了四个成员属性:capacity(容量),position(读写位置),mark(标记)

capacity属性

Buffer类的capacity属性,表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能在写入了。

capacity容量一旦初始化,就不能再改变。capacity不是指内存块byte[]数组的字节数量,是指写入数据对象的数量。

position属性

Buffer类的position属性,表示当前的位置。position属性与缓冲区的读写模式有关。

在写入模式下,position的值变化规则如下:

1.当缓冲区刚开始进入到读模式时,position会被重置为0.
2.当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
3.position的最大值为最大可读上限limit,当position到达limit时,表明缓冲区就没有空间可以写了。

在读模式下,position的值变化规则如下:

1.当缓冲区剋是进入到读模式时,position会被重置为0.
2.当从缓冲区读取时,也是从position位置开始读。读取数据后,position先前移动到下一个可读的位置。
3.position最大的值为最大可读上限limit,当position达到limit时,表明缓冲区已经无数据可读。
limit属性

Buffer类的limit属性,表示读写的最大上限。limit属性,也与缓冲区的读写模式有关。在不同的模式下,limit的值的含义时不同的。

在写模式下,limit属性值的含义为可以写入的数量最大上限。在刚进入到写模式时,limit的值会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满。

在读模式下,limit的值含义为最多能从缓冲区中读取到多少数据。

Buffer 类的重要方法

allocate()创建缓冲区

在使用Buffer(缓冲区)之前,首先需要获取Buffer子类的实例对象,并且分配内存空间。为了获取一个Buffer实例对象,不能使用子类构造器new创建一个实例对象,而时调用子类的allocate()方法。

put()写入到缓冲区

在调用allocate方法分配内存,返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象。要写入缓冲区,需要调用put()方法。put方法只有一个参数,即为所需要的对象。写入的数据类型要求与缓冲区的类型一致。

filp()翻转

向缓冲区写入数据后,不能直接从缓冲区中读取数据,需要调用filp()方法将写入模式转成读取模式。

get()从缓冲区读取

调用flip方法,将缓冲区切换成读取模式。在调用get方法就可以,每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。

rewind()倒带

已经读完的数据,如果需要在读一遍,可以调用rewind()方法。rewind()方法,主要是调整了缓冲区的position属性,规则如下:

1.position重置为0,所有可以重读缓冲区中的所有数据。
2.limit保持不变,数据量还是一样的。
3.nark标记被清理,表示之前的临时位置不能在用了。
mark()和reset()设置position位置

Buffer.mark()方法的作用是将当前position的值保存器来,放在mark属性中,让mark属性记住这个临时位置。Buffer.reset()方法将mark的值恢复到position中。

clear()清空缓冲区

在读取模式下,调用clear()方法将缓冲区切换成写入模式。此方法会将position清零,limit设置为capaity最大容量值,可以一直写下去,知道缓冲区写满。

通道(Channel)

在OIO中,同一个网络连接会关联到两个流:一个输入流,另一个是输出流。通过这两个流。不断地进行输入和输出操作。

在NIO中,同一个网络连接使用一个通道表示,所有的NIO的IO操作tong都是从通道开始的。一个通道类似与OIO中两个流的结合体,既可以从通道读取,也可以向通道写入。

常用的Channel有四种具体实现:FileChannel,SocketChannel,ServerSocketChannel,DataGramChannel.

FileChannel:文件通道,用于文件的数据读写。
SocketChannel:套接字通道,用于Socket套接字TCP连接的数据读写
ServerSocketChannel:服务器嵌套接字通道,允许监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel
DatagramChannel:数据报通道,用于UDP协议的数据读写。

FileChannel 文件通道

FileChannel是专门操作文件的通道。通过FileChannel,可以从一个文件读取数据,也可以将数据写入到文件中。FileChannel是阻塞模式,不能设置为非阻塞模式。

获取FileChannel通道

可以通过文件的输入流,输出流获取FileChannel文件通道。也可以通过RandomAccessFile文件随机访问类,获取FileChannel文件通道。

读取FileChannel通道

从通道读取数据都会调用通道的int read(ByteBuffer buf) 方法,从通道读取到数据写入到ByteBuffer缓冲区,并且返回读取到的数据量

注意:对于Channel来说是读取数据,但是对于ByteBuffer缓冲区来说是写入数据,这时候ByteBuffer缓冲区处于写入模式。

写入FileChannel

写入数据到通道,都会调用通道的int write(ByteBuffer buf) write方法的作用是,从ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数。

注意:此时的ByteBuffer缓冲区要求的是可读的,处于读模式下。

关闭通道

当通道使用完成后,必须将器关闭。调用close方法即可。

强制刷新到磁盘

将缓冲区写入通道是,由于性能原因,操作系统不可能每次都实时将数据写入磁盘。如果需要保证写入通道的缓冲数据,最终都真正的写入磁盘。可以调用FileChannel的force()方法。

SocketChannel和ServerSocketChannel套接字通道

在NIO中,涉及网络连接的通道有两个,一个是SocketChannel复制连接传输,另一个是ServerSocketChannel负责连接的监听。

ServerSocketSocket应用与服务器端,而SocketChannel同时处于服务器端和客户端。

无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种模式。设置方法如下:

1.socketChannel.configureBlocking(false)//设置为非阻塞模式
2.socketChannel.configureBlocking(true)

阻塞模式下,SocketChannel通道的connect连接,read,write都是同步和阻塞时的,与OIO相同。所有下面的以非阻塞的特点。

获取SocketChannel传输通道

客户端

通过SocketChannel静态方法open()获得一个套接字传输通道;然后将socket设置成为非阻塞的;最后通过connect()实例化方法,对服务器IP和端口发起连接。

SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
while(! socketChannel.finishConnect()){
    //不断自旋 等待。或者做一些其他的事情
}

服务端

当新连接事件到来时,在服务器端的ServerSocketChannel能够成功查询出一个新连接事件,并且通过调用服务器端ServerSocketChannel监听套接字accpet()方法,来获取连接套接字通道。

  ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        SocketChannel accept = serverSocketChannel.accept();
        accept.configureBlocking(false);
读取SocketChannel传输通道

当SocketChannel可读时,可以从SocketChannel读取数据,调用read方法将数据读入缓冲区

 ByteBuffer buffer = ByteBuffer.allocate(1024);
        int read = accept.read(buffer);

非阻塞的读取数据请开Selector章节

写入SocketChannel传输通道

调用读方法

buffer.flip();//将缓冲区变成读取模式
socketChannel.write(buffer)
关闭SocketChannel传输通道

在关闭SockerChannel传输通道签,如果传输通道用来写入数据,则建议一次shutdownOutput()种植输出方法,向对方发送一个输出结束标志(-1).然后调用socketChannel.close()方法,关闭套接字连接。

DatagramChannel数据报通道

DatagramChannel是UDP传输的。只需要知道对方的IP和端口就可传输数据

获取DatagramChannel
  DatagramChannel channel = DatagramChannel.open();
  channel.configureBlocking(false);
  channel.socket().bind(new InetSocketAddress(15555));

读取DatagramChannel

阻塞状态:需要调用SocketAddress receive(ByteBuffer buf)

ByteBuffer buffer = ByteBuffer.allocate(1222);
SocketAddress socketAddress = channel.receive(buffer);

非阻塞状态见Select选择器

写入DatagramChannel

调用send(方法)

  buffer.flip();
  channel.send(buffer,socketAddress);
  buffer.clear();

Selector选择器

为了实现IO多路复用,首先把Channel注册到Selector选择器中,然后通过选择器内部机制,可以查询select这些注册的通道是否有已经就绪的IO事件。

与OIO相比,使用选择器的最大优势: 系统开销小,系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减小了系统的开销。

一个单线程处理一个Selector选择器,一个选择器可以监控很多Channel。通过Selector,一个线程可以处理数百,数千,数万,甚至更多的通道。

Channel和Selecotr之间的关系,通过register的方法完成。调用Channel的register(Selector sel,int ops)方法

可以选择Selelctor的Channel的IO事件类型,包括以下四种:

1.可读:SelectionKey.OP_READ
2.可写:SelectionKey.OP_WRITE
3.连接:SelectionKey.OP_CONNECT
4.接收:SelectionKey.OP_ACCEPT

事件类型定义在SlectionKey类中。如果选择器要监控Channel的多种事件,可以用“|”来实现

int key = SelectionKey.OP_READ | SelectorKey.OP_WRITE
SelectableChannel 可选通道

不是所有的Channel都能被Selelctor选择器监控的。只有继承了SelectableChannel,才可以被选择。

SelectionKey选择键

Channel和Selector的监控关系注册后,就可以选择就绪事件。具体的选择工作,和调用选择器Selector的select()方法来完成。通过select方法,选择器可以不断地选择Channel中所发生操作的就绪状态,返回注册过的感兴趣的那些IO事件。

SelectionKey是那些被Selector选中的IO事件。一个IO事件发生后,如果之前在Selector中注册过 ,就会被Selector选中,并放入SelelctorKey集合中;如果没有注册过,即使发生了IO事件,也不会被Selector选中。

Selector使用流程

步骤如下:

1. 获取选择器实例
2.将通道注册到选择器中;
3.轮询注册的IO就绪事件

Selector 的类方法open()的内部,是向选择器SPI(SelecotrProvider)发出请求,通过默认的SelectorProvide对象,获取一个新的Selector实例。Java中SPI全称为(Service Provider Interface,服务提供者接口),是jdk的一种可以扩展的服务提供和发现机制。

Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(99999));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while(selector.select()>0){
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()){
                    //io事件 ServerSocketChannel服务器监听通道有新连接
                }else if (selectionKey.isConnectable()){
                    //IO事件 传输通道连接成功
                }else if (selectionKey.isReadable()){
                    //IO事件 传输通道可读
                }else if (selectionKey.isWritable()){
                    //IO事件 传输通道可读
                }
                iterator.remove();
            }
        }

参考书籍
《Netty,Redis,Zookeeper高并发实战》

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐